{
  "openapi": "3.1.0",
  "info": {
    "title": "Parky API (Web App)",
    "version": "0.1.0",
    "description": "Parky API — モバイル Web 版 / 公開 Web (parky.co.jp / dev.parky.co.jp) 向けエンドポイント。optional auth / 認証ユーザー必須エンドポイントを含む。"
  },
  "servers": [
    {
      "url": "https://api.parky.co.jp",
      "description": "Production"
    },
    {
      "url": "https://stg-api.parky.co.jp",
      "description": "Staging"
    },
    {
      "url": "https://dev-api.parky.co.jp",
      "description": "Development"
    },
    {
      "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"
        }
      },
      "CodeMetadata": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "color": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": {}
      },
      "CodeRow": {
        "type": "object",
        "properties": {
          "category_id": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "display_label": {
            "type": "string"
          },
          "lang": {
            "type": "string"
          },
          "sort_order": {
            "type": "integer"
          },
          "metadata": {
            "$ref": "#/components/schemas/CodeMetadata"
          }
        },
        "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"
                ]
              },
              "message_key": {
                "type": "string",
                "examples": [
                  "common.error.not_found"
                ]
              },
              "request_id": {
                "type": "string",
                "examples": [
                  "7d4e5…-…"
                ]
              },
              "param": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "リクエスト上のエラー対象フィールド名（zod path[0] 等）。root レベルエラーは null。",
                "examples": [
                  "email"
                ]
              },
              "doc_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "エラーコードに対応するドキュメント URL。未整備時は null。",
                "examples": [
                  null
                ]
              }
            },
            "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"
        ]
      },
      "SearchPreferences": {
        "type": "object",
        "properties": {
          "is_premium": {
            "type": "boolean",
            "description": "プレミアムユーザーか。無料ユーザーは PUT で 403 を受ける",
            "examples": [
              true
            ]
          },
          "yen_per_walk_minute": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500,
            "description": "徒歩1分あたりの金銭換算値（円/分）",
            "examples": [
              100
            ]
          },
          "is_default": {
            "type": "boolean",
            "description": "DB に保存値が無いデフォルト状態か。true ならサーバ合成値",
            "examples": [
              false
            ]
          }
        },
        "required": [
          "is_premium",
          "yen_per_walk_minute",
          "is_default"
        ]
      },
      "SearchPreferencesUpdate": {
        "type": "object",
        "properties": {
          "yen_per_walk_minute": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500
          }
        },
        "required": [
          "yen_per_walk_minute"
        ]
      },
      "ParkingSessionRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "user_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "user_id",
          "parking_lot_id",
          "status",
          "created_at"
        ]
      },
      "ParkingSession": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ParkingSessionRow"
          },
          {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "string",
                "format": "uuid"
              },
              "vehicle_type": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "started_at": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ended_at": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "total_amount_minor": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "memo": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "personal_rating": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "start_lat": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "start_lng": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "vehicle_type",
              "started_at",
              "ended_at",
              "total_amount_minor",
              "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"
        ]
      },
      "ReviewStatus": {
        "type": "string",
        "enum": [
          "pending",
          "approved",
          "rejected",
          "hidden"
        ]
      },
      "ReviewRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "user_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "comment": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/ReviewStatus"
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "parking_lot_id",
          "user_id",
          "rating",
          "comment",
          "status",
          "created_at"
        ]
      },
      "MyReview": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReviewRow"
          },
          {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "string",
                "format": "uuid"
              }
            }
          }
        ]
      },
      "PublicReview": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReviewRow"
          },
          {
            "type": "object",
            "properties": {
              "user_name": {
                "type": "string"
              }
            },
            "required": [
              "user_name"
            ]
          }
        ]
      },
      "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"
        ]
      },
      "SubscriptionPlanFeatures": {
        "anyOf": [
          {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          {
            "type": "object",
            "additionalProperties": {}
          }
        ]
      },
      "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": {
            "$ref": "#/components/schemas/SubscriptionPlanFeatures"
          },
          "sort_order": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "code",
          "name",
          "description",
          "price_minor",
          "currency",
          "billing_period",
          "accent_color",
          "features",
          "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"
          },
          "next_billing_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "次回課金予定日 (ISO8601)。自動更新 OFF / 単発購入は null。"
          },
          "is_trial_period": {
            "type": "boolean",
            "description": "無料トライアル期間中か。ios は offerType=1+FREE_TRIAL、android は offerId に trial 含有で判定。"
          },
          "auto_renewing": {
            "type": "boolean",
            "description": "自動更新が ON か。ios: revocation / expires 過去なら false、android: lineItems[0].autoRenewingPlan.autoRenewEnabled。"
          },
          "original_purchase_date": {
            "type": "string",
            "description": "最初の購入日（サブスク開始日）。recurring 更新でも不変。"
          },
          "provider": {
            "type": "string",
            "enum": [
              "apple",
              "google"
            ],
            "description": "検証プロバイダ。platform=ios↔apple、platform=android↔google に 1:1 対応。"
          }
        },
        "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",
          "next_billing_date",
          "is_trial_period",
          "auto_renewing",
          "original_purchase_date",
          "provider"
        ]
      },
      "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"
        ]
      },
      "GeoJSONGeometry": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Point",
              "LineString",
              "Polygon",
              "MultiPoint",
              "MultiLineString",
              "MultiPolygon",
              "GeometryCollection"
            ]
          },
          "coordinates": {},
          "geometries": {
            "type": "array",
            "items": {}
          }
        },
        "required": [
          "type"
        ],
        "additionalProperties": {}
      },
      "ParkingLot": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "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"
            ]
          },
          "created_at": {
            "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"
            ]
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "shape_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "area": {
            "$ref": "#/components/schemas/GeoJSONGeometry"
          },
          "operator_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "entry_difficulty": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "address",
          "lat",
          "lng",
          "total_spaces",
          "status",
          "structure",
          "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位バッジ用フラグ"
              }
            }
          }
        ]
      },
      "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"
        ]
      },
      "ParkingLotDetailImage": {
        "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"
            ]
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "公開 URL (R2_PUBLIC_BASE_URL + s3_key)。asset 削除済 / 未配置の場合は null。"
          }
        },
        "required": [
          "id",
          "parking_lot_id",
          "asset_id",
          "is_main",
          "sort_order",
          "image_url"
        ]
      },
      "ParkingLotTag": {
        "type": "object",
        "properties": {
          "tag_id": {
            "type": "string",
            "format": "uuid"
          },
          "state": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "tag": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "category": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "icon_name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "color",
              "slug",
              "category"
            ]
          }
        },
        "required": [
          "tag_id",
          "tag"
        ]
      },
      "ParkingLotDetailOperator": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "color": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "color"
        ]
      },
      "ParkingLotFieldValueIds": {
        "type": "object",
        "additionalProperties": {
          "type": "string",
          "format": "uuid"
        },
        "description": "include=field_value_ids 指定時に返る per-field の primary parking_field_values.id。web/home の ✓/✗ tap ボタンが POST 先 endpoint の path param として利用する。"
      },
      "ParkingLotPublicSpot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "code": {
            "type": "string"
          },
          "vehicle_type_max": {
            "type": [
              "string",
              "null"
            ],
            "description": "対応車種上限 (codes(category='vehicle_type') の code)"
          },
          "is_ev_charger": {
            "type": "boolean"
          },
          "ev_connector_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "EV コネクタ種別 (codes(category='ev_connector_type'))"
          },
          "accessibility": {
            "type": [
              "string",
              "null"
            ],
            "description": "アクセシビリティ属性 (codes(category='accessibility')、例: wheelchair / parent_child)"
          },
          "is_reservable": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "code",
          "vehicle_type_max",
          "is_ev_charger",
          "ev_connector_type",
          "accessibility",
          "is_reservable"
        ]
      },
      "ParkingLotPublicPricingGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "code": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean"
          },
          "display_order": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "code",
          "name",
          "is_default",
          "display_order"
        ]
      },
      "ParkingLotWithDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ParkingLot"
          },
          {
            "type": "object",
            "properties": {
              "pricing_rules": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PricingRule"
                }
              },
              "images": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ParkingLotDetailImage"
                }
              },
              "tags": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ParkingLotTag"
                }
              },
              "operator": {
                "$ref": "#/components/schemas/ParkingLotDetailOperator"
              },
              "field_value_ids": {
                "$ref": "#/components/schemas/ParkingLotFieldValueIds"
              },
              "is_open_now": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "現在営業中かどうか（null = 営業時間未設定 or RPC 失敗）。後方互換フィールド。"
              },
              "derived": {
                "type": "object",
                "properties": {
                  "is_open_now": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "can_enter_now": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  },
                  "can_exit_now": {
                    "type": [
                      "boolean",
                      "null"
                    ]
                  }
                },
                "required": [
                  "is_open_now",
                  "can_enter_now",
                  "can_exit_now"
                ],
                "description": "RPC から派生した営業・入出庫可否フラグ。2026-04-24 追加。従来の flat `is_open_now` も並存して返す（後方互換）。"
              },
              "hours": {
                "type": "object",
                "properties": {
                  "windows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "window_type": {
                          "type": "string",
                          "enum": [
                            "business",
                            "entry",
                            "exit",
                            "after_hours_exit"
                          ]
                        },
                        "day_type": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "weekday",
                            "saturday",
                            "sunday",
                            "holiday",
                            "holiday_eve",
                            "all"
                          ]
                        },
                        "day_of_week": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "is_24h": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        },
                        "is_closed": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        },
                        "open_time": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "close_time": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "effective_from": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "effective_to": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "id",
                        "window_type",
                        "day_type",
                        "day_of_week",
                        "is_24h",
                        "is_closed",
                        "open_time",
                        "close_time",
                        "effective_from",
                        "effective_to"
                      ]
                    }
                  },
                  "legacy_business": {
                    "type": "array",
                    "items": {}
                  }
                },
                "required": [
                  "windows"
                ],
                "description": "営業時間 windows 配列（window_type × day_type × 期間）。2026-04-24 追加。`include=hours` で有効。"
              },
              "date_overrides": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "override_date": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "hours": {},
                    "pricing": {},
                    "note": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "override_date",
                    "label",
                    "note"
                  ]
                },
                "description": "当日〜30 日先の日付オーバーライド配列。2026-04-24 追加。`include=date_overrides` で有効。"
              },
              "regulations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "regulation_type": {
                      "type": "string"
                    },
                    "fee_yen": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "time_limit_min": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "badge_only": {
                      "type": "boolean"
                    },
                    "motorcycle_ok": {
                      "type": "boolean"
                    },
                    "normal_car_ok": {
                      "type": "boolean"
                    },
                    "cargo_ok": {
                      "type": "boolean"
                    },
                    "has_restriction": {
                      "type": "boolean"
                    },
                    "overlap_regulations": {},
                    "external_id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "source": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "regulation_type",
                    "fee_yen",
                    "time_limit_min",
                    "badge_only",
                    "motorcycle_ok",
                    "normal_car_ok",
                    "cargo_ok",
                    "has_restriction",
                    "external_id",
                    "source"
                  ]
                },
                "description": "tokyometer 等の駐車規制データ (パーキング・チケット / メーター)。Phase 0.5+ で追加 (master.rules から parking_lot_regulations に分離)。`include=regulations` で有効。"
              },
              "highlighted_fields": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "検索条件（highlight_* クエリパラメータ）にマッチしたセクション名リスト。 検索結果 → 詳細画面遷移時に UI のハイライト対象を判定するために返す。 値の例: `[\"pricing\", \"roof\", \"operating_hours\", \"features\", \"vehicle_type\"]`。 highlight_* QP が 1 つも指定されない場合は field 自体が省略される。"
              },
              "highlighted_tag_slugs": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "検索条件にマッチしたタグ slug の具体リスト（features / roof / open_24h の UI チップ個別ハイライト用）。 `highlighted_fields` がセクション単位なのに対し、この field はタグ単位で「どのチップを光らせるか」を示す。 roof_only / open_24h の alias 展開後の実在 slug（例: `covered` / `roof` / `indoor` / `open_24h`）を含む。"
              },
              "spots": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ParkingLotPublicSpot"
                },
                "description": "駐車場内の個別車室一覧（公開可能属性のみ）。soft delete (deleted_at) と is_active=false は除外。 EV 充電器・アクセシビリティ・対応車種など UI の「設備」表示や検索適合チェックに使う。 1 駐車場で複数 spot を持たないレガシー駐車場では空配列。"
              },
              "pricing_groups": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ParkingLotPublicPricingGroup"
                },
                "description": "駐車場内の料金プラン (pricing group) 一覧。display_order ASC, code ASC 順。 is_default=true の group が UI の主表示用。複数あれば「他の料金プラン」セクションを出す。 個別 group の rule は既存の `include=pricing_rules` から pricing_group_id で照合。"
              }
            }
          }
        ]
      },
      "Review": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReviewRow"
          },
          {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "string",
                "format": "uuid"
              },
              "owner_reply": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "オーナーからの返信テキスト（未返信時は null）"
              },
              "owner_replied_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "オーナー返信日時（ISO8601）"
              }
            },
            "required": [
              "owner_reply",
              "owner_replied_at"
            ]
          }
        ]
      },
      "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"
                ]
              },
              "city_slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prefecture_slug": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "code",
              "lat",
              "lng",
              "city_slug",
              "prefecture_slug"
            ]
          }
        },
        "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"
        ]
      },
      "FeeCalcBreakdownItem": {
        "type": "object",
        "properties": {
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "time_start": {
            "type": [
              "string",
              "null"
            ]
          },
          "time_end": {
            "type": [
              "string",
              "null"
            ]
          },
          "minutes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "amount_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cap_applied": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": {}
      },
      "FeeCalcResponse": {
        "type": "object",
        "properties": {
          "total_amount_minor": {
            "type": "integer"
          },
          "breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeeCalcBreakdownItem"
            }
          },
          "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"
        ]
      },
      "FieldTapResponse": {
        "type": "object",
        "properties": {
          "tap_id": {
            "type": "string",
            "format": "uuid"
          },
          "confirms_n": {
            "type": [
              "integer",
              "null"
            ],
            "description": "is_correct=true 時の更新後の confirms_n。false 時は null。"
          }
        },
        "required": [
          "tap_id",
          "confirms_n"
        ]
      },
      "FieldTapBody": {
        "type": "object",
        "properties": {
          "is_correct": {
            "type": "boolean",
            "description": "✓ 合ってる = true / ✗ 違う = false。true 時のみ parking_field_values.confirms_n を +1。"
          }
        },
        "required": [
          "is_correct"
        ]
      },
      "ParkingDetailBffData": {
        "type": "object",
        "properties": {
          "lot": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lat": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "lng": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "status": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "total_spaces": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "updated_at": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name",
              "address",
              "lat",
              "lng",
              "status",
              "total_spaces",
              "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"
            ]
          },
          "derived": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "is_open_now": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "can_enter_now": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "can_exit_now": {
                "type": [
                  "boolean",
                  "null"
                ]
              }
            },
            "required": [
              "is_open_now",
              "can_enter_now",
              "can_exit_now"
            ]
          }
        },
        "required": [
          "lot",
          "reviews",
          "pricing_rules",
          "nearby",
          "is_open_now"
        ]
      },
      "UiConfig": {
        "type": "object",
        "properties": {
          "messages": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "feature_flags": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "experiment_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "theme_hint": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "auto"
            ],
            "default": "auto"
          },
          "highlighted_fields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "NavigationHint": {
        "type": "object",
        "properties": {
          "target": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "params": {
            "type": "object",
            "additionalProperties": {}
          },
          "strategy": {
            "type": "string",
            "enum": [
              "push",
              "replace",
              "pop_to_root"
            ],
            "default": "push"
          }
        },
        "required": [
          "target"
        ]
      },
      "ValidationRule": {
        "type": "object",
        "properties": {
          "field": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "rule": {
            "type": "string",
            "enum": [
              "required",
              "min_length",
              "max_length",
              "regex",
              "numeric_range"
            ]
          },
          "param": {},
          "message_code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          }
        },
        "required": [
          "field",
          "rule",
          "message_code"
        ]
      },
      "ErrorState": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "message_code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "retryable": {
            "type": "boolean"
          },
          "fallback": {
            "type": "string",
            "enum": [
              "cached",
              "empty",
              "block"
            ]
          }
        },
        "required": [
          "code",
          "message_code",
          "retryable",
          "fallback"
        ]
      },
      "EmptyState": {
        "type": "object",
        "properties": {
          "illustration": {
            "type": [
              "string",
              "null"
            ]
          },
          "title_code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "body_code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "cta": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "label_code": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              },
              "action": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              }
            },
            "required": [
              "label_code",
              "action"
            ]
          }
        },
        "required": [
          "illustration",
          "title_code",
          "body_code",
          "cta"
        ]
      },
      "SkeletonHint": {
        "type": "object",
        "properties": {
          "layout": {
            "type": "string",
            "enum": [
              "list",
              "detail",
              "map",
              "grid",
              "splash"
            ]
          },
          "item_count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50
          }
        },
        "required": [
          "layout"
        ]
      },
      "ViewStates": {
        "type": "object",
        "properties": {
          "error": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ErrorState"
            }
          },
          "empty": {
            "$ref": "#/components/schemas/EmptyState"
          },
          "skeleton": {
            "$ref": "#/components/schemas/SkeletonHint"
          }
        }
      },
      "FallbackBehavior": {
        "type": "object",
        "properties": {
          "on_network_error": {
            "type": "string",
            "enum": [
              "show_cached",
              "show_empty",
              "show_error"
            ]
          },
          "on_auth_error": {
            "type": "string",
            "enum": [
              "redirect_login",
              "show_error"
            ]
          },
          "on_version_mismatch": {
            "type": "string",
            "enum": [
              "force_update",
              "degrade",
              "ignore"
            ]
          },
          "cache_ttl_seconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400
          }
        },
        "required": [
          "on_network_error",
          "on_auth_error",
          "on_version_mismatch",
          "cache_ttl_seconds"
        ]
      },
      "RealtimeHint": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "INSERT",
                "UPDATE",
                "DELETE"
              ]
            },
            "minItems": 1
          },
          "rls_precondition": {
            "type": "string",
            "maxLength": 256
          },
          "fallback_poll_seconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 3600,
            "default": 0
          }
        },
        "required": [
          "channel",
          "event_types"
        ]
      },
      "ViewMeta": {
        "type": "object",
        "properties": {
          "server_time": {
            "type": "string",
            "format": "date-time"
          },
          "cache_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "min_app_version": {
            "type": "string",
            "pattern": "^\\d+\\.\\d+\\.\\d+$"
          },
          "sunset_date": {
            "type": [
              "string",
              "null"
            ]
          },
          "realtime": {
            "$ref": "#/components/schemas/RealtimeHint"
          },
          "expected_ui_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "UI バージョン鮮度トークン。`/boot` レスポンスに含まれる `ui_layer.ui_version` と突合し、不一致ならクライアントは背景で再 fetch する。null は UI レイヤー情報を返さないエンドポイント。",
            "examples": [
              "ui_2026_04_26_1547"
            ]
          },
          "view_spec_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "/boot の ui_layer.view_specs[id] への参照 key。client は spec を cache resolve して validation/states/fallback_behavior を取得。null = spec 未カタログ化 endpoint",
            "examples": [
              "parking_lot_detail_v1"
            ]
          }
        },
        "required": [
          "server_time",
          "cache_key",
          "min_app_version",
          "sunset_date"
        ]
      },
      "ParkingDetailBff": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ParkingDetailBffData"
          },
          "ui_config": {
            "$ref": "#/components/schemas/UiConfig"
          },
          "navigation": {
            "$ref": "#/components/schemas/NavigationHint"
          },
          "validation": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ValidationRule"
            }
          },
          "states": {
            "$ref": "#/components/schemas/ViewStates"
          },
          "fallback_behavior": {
            "$ref": "#/components/schemas/FallbackBehavior"
          },
          "meta": {
            "$ref": "#/components/schemas/ViewMeta"
          }
        },
        "required": [
          "data",
          "fallback_behavior",
          "meta"
        ]
      },
      "ParkingSessionCreateResponse": {
        "type": "string",
        "format": "uuid",
        "description": "作成された parking_sessions.id (uuid)。冪等再送時も同じ id が返る。"
      },
      "ParkingFinalizeReward": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "exp_granted": {
            "type": [
              "number",
              "null"
            ]
          },
          "exp_base": {
            "type": [
              "number",
              "null"
            ]
          },
          "multiplier": {
            "type": [
              "number",
              "null"
            ]
          },
          "total_exp": {
            "type": [
              "number",
              "null"
            ]
          },
          "total_exp_before": {
            "type": [
              "number",
              "null"
            ]
          },
          "level_before": {
            "type": [
              "number",
              "null"
            ]
          },
          "level_after": {
            "type": [
              "number",
              "null"
            ]
          },
          "level_up": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "daily_cap_hit": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "badges_earned": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "properties": {
                "badge_id": {
                  "type": "string"
                },
                "id": {
                  "type": "string"
                }
              }
            }
          },
          "geo_verified": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ParkingSessionFinalizeResponse": {
        "type": "object",
        "properties": {
          "session_id": {
            "type": "string",
            "format": "uuid"
          },
          "total_amount_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "user_entered_fee_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "fee_mismatch_flag": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "breakdown": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FeeCalcBreakdownItem"
            }
          },
          "idempotent": {
            "type": "boolean"
          },
          "reward": {
            "$ref": "#/components/schemas/ParkingFinalizeReward"
          }
        },
        "required": [
          "session_id",
          "total_amount_minor",
          "ended_at"
        ]
      },
      "ParkingSessionCancelResponse": {
        "type": "object",
        "properties": {
          "session_id": {
            "type": "string",
            "format": "uuid"
          },
          "cancelled": {
            "type": "boolean"
          },
          "idempotent": {
            "type": "boolean"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "session_id",
          "cancelled"
        ]
      },
      "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"
        ]
      },
      "RelatedHub": {
        "type": "object",
        "properties": {
          "prefSlug": {
            "type": "string"
          },
          "citySlug": {
            "type": "string"
          },
          "spotSlug": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "prefSlug",
          "citySlug",
          "spotSlug"
        ]
      },
      "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": {
              "$ref": "#/components/schemas/RelatedHub"
            }
          },
          "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"
        ]
      },
      "BatchUploadUrlResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UploadUrlResponse"
            }
          }
        },
        "required": [
          "items"
        ]
      },
      "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"
        ]
      },
      "PricingRuleRaw": {
        "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": {
            "type": [
              "integer",
              "null"
            ]
          },
          "price_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cap_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "cap_duration_hours": {
            "type": [
              "number",
              "null"
            ]
          },
          "cap_price": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cap_price_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cap_repeat": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "cap_scope": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "parking_lot_id",
          "category"
        ],
        "additionalProperties": {}
      },
      "SearchLot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "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"
            ]
          },
          "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": {
            "$ref": "#/components/schemas/GeoJSONGeometry"
          },
          "operator_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "operator_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "entry_difficulty": {
            "type": [
              "string",
              "null"
            ]
          },
          "pricing_rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PricingRuleRaw"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParkingLotTag"
            }
          },
          "pricing_groups_count": {
            "type": "integer",
            "description": "登録されている料金プラン数 (>=2 でバッジ表示)"
          },
          "is_24h": {
            "type": "boolean",
            "description": "24時間営業フラグ (parking_lot_hours.business window 集約)"
          }
        },
        "required": [
          "id",
          "name",
          "address",
          "lat",
          "lng",
          "total_spaces",
          "status",
          "structure",
          "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",
          "area",
          "operator_code",
          "operator_name",
          "entry_difficulty",
          "pricing_rules",
          "tags",
          "pricing_groups_count",
          "is_24h"
        ]
      },
      "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",
              "null"
            ]
          },
          "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"
            ]
          },
          "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"
            ]
          },
          "main_image_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "pricing_rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PricingRuleRaw"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParkingLotTag"
            }
          }
        },
        "required": [
          "id",
          "name",
          "address",
          "lat",
          "lng",
          "total_spaces",
          "status",
          "structure",
          "max_height_m",
          "max_width_m",
          "max_length_m",
          "max_weight_t",
          "min_clearance_cm",
          "operator_code",
          "operator_name",
          "entry_difficulty",
          "main_image_url",
          "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"
            ]
          },
          "walking_cost_yen_per_minute": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500,
            "description": "検索おすすめ順の既定: 徒歩1分あたり何円と換算するか（0〜500）",
            "examples": [
              17
            ]
          },
          "meters_per_walk_minute": {
            "type": "integer",
            "minimum": 30,
            "maximum": 200,
            "description": "徒歩速度（m/分）",
            "examples": [
              80
            ]
          },
          "round_trip_multiplier": {
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "往復倍率",
            "examples": [
              2
            ]
          },
          "updated_at": {
            "type": "string"
          }
        },
        "required": [
          "min_app_version_ios",
          "min_app_version_android",
          "is_maintenance",
          "maintenance_message",
          "store_url_ios",
          "store_url_android",
          "walking_cost_yen_per_minute",
          "meters_per_walk_minute",
          "round_trip_multiplier",
          "updated_at"
        ]
      },
      "JpHoliday": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "national",
              "substitute",
              "national_holiday_bridge"
            ]
          }
        },
        "required": [
          "date",
          "name",
          "kind"
        ]
      },
      "DataExportProfile": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "display_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_active_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DataExportParkingSession": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "vehicle_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "ended_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "total_amount_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "memo": {
            "type": [
              "string",
              "null"
            ]
          },
          "personal_rating": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "parking_lot_id"
        ]
      },
      "DataExportReview": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "rating": {
            "type": "integer"
          },
          "comment": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "parking_lot_id",
          "rating"
        ],
        "additionalProperties": {}
      },
      "DataExportRating": {
        "type": "object",
        "properties": {
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "rating": {
            "type": "integer"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "parking_lot_id",
          "rating"
        ],
        "additionalProperties": {}
      },
      "DataExportSavedLot": {
        "type": "object",
        "properties": {
          "parking_lot_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "parking_lot_id"
        ],
        "additionalProperties": {}
      },
      "DataExportVehicle": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "vehicle_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "registration_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id"
        ],
        "additionalProperties": {}
      },
      "DataExportSearchPreset": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "query_json": {}
        },
        "required": [
          "id"
        ],
        "additionalProperties": {}
      },
      "DataExport": {
        "type": "object",
        "properties": {
          "export_date": {
            "type": "string"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "profile": {
            "$ref": "#/components/schemas/DataExportProfile"
          },
          "parking_sessions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportParkingSession"
            }
          },
          "reviews": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportReview"
            }
          },
          "ratings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportRating"
            }
          },
          "saved_parking_lots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportSavedLot"
            }
          },
          "vehicles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportVehicle"
            }
          },
          "search_presets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DataExportSearchPreset"
            }
          },
          "registered_device_count": {
            "type": "number"
          }
        },
        "required": [
          "export_date",
          "user_id",
          "profile",
          "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"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed"
            ]
          },
          "note": {
            "type": "string"
          },
          "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"
        ]
      },
      "DevicePermissionsResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "ok",
          "recorded_at"
        ]
      },
      "DevicePlatform": {
        "type": "string",
        "enum": [
          "ios",
          "android"
        ]
      },
      "DeviceLocationStatus": {
        "type": "string",
        "enum": [
          "granted_always",
          "granted_when_in_use",
          "denied",
          "not_determined"
        ]
      },
      "DevicePushStatus": {
        "type": "string",
        "enum": [
          "granted",
          "denied",
          "not_determined"
        ]
      },
      "DevicePermissionsBody": {
        "type": "object",
        "properties": {
          "device_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "examples": [
              "fcm-token-abc123"
            ]
          },
          "platform": {
            "$ref": "#/components/schemas/DevicePlatform"
          },
          "location_status": {
            "$ref": "#/components/schemas/DeviceLocationStatus"
          },
          "push_status": {
            "$ref": "#/components/schemas/DevicePushStatus"
          },
          "app_version": {
            "type": "string",
            "maxLength": 64,
            "examples": [
              "1.3.2"
            ]
          },
          "os_version": {
            "type": "string",
            "maxLength": 64,
            "examples": [
              "17.4"
            ]
          }
        },
        "required": [
          "device_id",
          "platform"
        ]
      },
      "PasswordPolicy": {
        "type": "object",
        "properties": {
          "minLength": {
            "type": "integer",
            "examples": [
              8
            ]
          },
          "maxLength": {
            "type": "integer",
            "examples": [
              128
            ]
          },
          "requireLowercase": {
            "type": "boolean",
            "examples": [
              true
            ]
          },
          "requireUppercase": {
            "type": "boolean",
            "examples": [
              true
            ]
          },
          "requireDigit": {
            "type": "boolean",
            "examples": [
              true
            ]
          },
          "requireSymbol": {
            "type": "boolean",
            "examples": [
              true
            ]
          },
          "allowedSymbols": {
            "type": "string",
            "examples": [
              "!@#$%^&*()_+-=[]{}|;:,.<>?/"
            ]
          },
          "messages": {
            "type": "object",
            "properties": {
              "ja": {
                "type": "object",
                "properties": {
                  "minLength": {
                    "type": "string"
                  },
                  "maxLength": {
                    "type": "string"
                  },
                  "requireLowercase": {
                    "type": "string"
                  },
                  "requireUppercase": {
                    "type": "string"
                  },
                  "requireDigit": {
                    "type": "string"
                  },
                  "requireSymbol": {
                    "type": "string"
                  }
                },
                "required": [
                  "minLength",
                  "maxLength",
                  "requireLowercase",
                  "requireUppercase",
                  "requireDigit",
                  "requireSymbol"
                ]
              },
              "en": {
                "type": "object",
                "properties": {
                  "minLength": {
                    "type": "string"
                  },
                  "maxLength": {
                    "type": "string"
                  },
                  "requireLowercase": {
                    "type": "string"
                  },
                  "requireUppercase": {
                    "type": "string"
                  },
                  "requireDigit": {
                    "type": "string"
                  },
                  "requireSymbol": {
                    "type": "string"
                  }
                },
                "required": [
                  "minLength",
                  "maxLength",
                  "requireLowercase",
                  "requireUppercase",
                  "requireDigit",
                  "requireSymbol"
                ]
              }
            },
            "required": [
              "ja",
              "en"
            ]
          }
        },
        "required": [
          "minLength",
          "maxLength",
          "requireLowercase",
          "requireUppercase",
          "requireDigit",
          "requireSymbol",
          "allowedSymbols",
          "messages"
        ]
      },
      "OAuthProvider": {
        "type": "string",
        "enum": [
          "google",
          "apple",
          "facebook"
        ]
      },
      "OtpConfig": {
        "type": "object",
        "properties": {
          "ttl_seconds": {
            "type": "integer",
            "examples": [
              300
            ]
          },
          "resend_cooldown_seconds": {
            "type": "integer",
            "examples": [
              60
            ]
          },
          "max_attempts": {
            "type": "integer",
            "examples": [
              5
            ]
          }
        },
        "required": [
          "ttl_seconds",
          "resend_cooldown_seconds",
          "max_attempts"
        ]
      },
      "LockoutConfig": {
        "type": "object",
        "properties": {
          "thresholds": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "examples": [
              [
                5,
                10,
                15
              ]
            ]
          },
          "durations_seconds": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "examples": [
              [
                900,
                3600,
                86400
              ]
            ]
          }
        },
        "required": [
          "thresholds",
          "durations_seconds"
        ]
      },
      "AuthConfig": {
        "type": "object",
        "properties": {
          "password_policy": {
            "$ref": "#/components/schemas/PasswordPolicy"
          },
          "oauth_providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OAuthProvider"
            }
          },
          "otp": {
            "$ref": "#/components/schemas/OtpConfig"
          },
          "lockout": {
            "$ref": "#/components/schemas/LockoutConfig"
          }
        },
        "required": [
          "password_policy",
          "oauth_providers",
          "otp",
          "lockout"
        ]
      },
      "PreflightStatus": {
        "type": "string",
        "enum": [
          "available",
          "exists_with_password",
          "exists_with_oauth",
          "withdrawn_rejoinable",
          "blocked"
        ]
      },
      "PreflightResponse": {
        "type": "object",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/PreflightStatus"
          },
          "provider": {
            "type": "string",
            "enum": [
              "google",
              "apple",
              "facebook"
            ]
          },
          "locked_until": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "status"
        ]
      },
      "PreflightBody": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email"
          }
        },
        "required": [
          "email"
        ]
      },
      "LoginFailureResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "count": {
            "type": "integer"
          },
          "locked_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "ok",
          "count",
          "locked_until"
        ]
      },
      "LoginResultBody": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email"
          }
        },
        "required": [
          "email"
        ]
      },
      "LoginSuccessResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "ok"
        ]
      },
      "ClientEventCreated": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "created_at"
        ]
      },
      "OwnerInquiryCreated": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "deduplicated": {
            "type": "boolean",
            "description": "直近 60 分以内の同条件申込を検出して既存を返したか。"
          }
        },
        "required": [
          "id",
          "status",
          "deduplicated"
        ]
      },
      "OwnerInquirySubmitBody": {
        "type": "object",
        "properties": {
          "owner_type": {
            "type": "string",
            "enum": [
              "corporate",
              "individual"
            ],
            "description": "事業者種別。corporate=法人 / individual=個人"
          },
          "inquiry_kind": {
            "type": "string",
            "enum": [
              "new_listing",
              "ownership_transfer"
            ],
            "default": "new_listing",
            "description": "申込の性質。new_listing=時間貸し駐車場の新規掲載 / ownership_transfer=既掲載駐車場のオーナー運用切替"
          },
          "company_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 120,
            "description": "法人名（owner_type=corporate のとき必須）"
          },
          "company_name_kana": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "representative_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "contact_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "contact_name_kana": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "contact_email": {
            "type": "string",
            "maxLength": 160,
            "format": "email"
          },
          "contact_phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20
          },
          "contact_address": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "lot_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "lot_address": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "lot_capacity": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 9999
          },
          "ownership_type": {
            "type": "string",
            "enum": [
              "owned",
              "leased",
              "managed",
              "other"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "consent_documents": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "書類提出同意。必ず true でなければならない。"
          },
          "consent_privacy": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "プライバシーポリシー同意。必ず true でなければならない。"
          }
        },
        "required": [
          "owner_type",
          "contact_name",
          "contact_email",
          "contact_phone",
          "lot_name",
          "lot_address",
          "ownership_type",
          "consent_documents",
          "consent_privacy"
        ]
      },
      "OwnerInquiryUploadTokenSummary": {
        "type": "object",
        "properties": {
          "token_valid": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "inquiry": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "contact_name": {
                "type": "string"
              },
              "contact_email": {
                "type": "string"
              },
              "lot_name": {
                "type": "string"
              },
              "owner_type": {
                "type": "string"
              },
              "inquiry_kind": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "現在の申込ステータス。documents_submitted 以降は UI で完了画面を出すために使う。"
              }
            },
            "required": [
              "id",
              "contact_name",
              "contact_email",
              "lot_name",
              "owner_type",
              "inquiry_kind",
              "status"
            ]
          },
          "asset_count": {
            "type": "integer",
            "description": "現在この申込に紐付いているアップロード済書類の件数。"
          },
          "expires_at": {
            "type": "string"
          }
        },
        "required": [
          "token_valid",
          "inquiry",
          "asset_count",
          "expires_at"
        ]
      },
      "OwnerInquiryUploadUrlResponse": {
        "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"
        ]
      },
      "OwnerInquiryUploadUrlBody": {
        "type": "object",
        "properties": {
          "doc_kind": {
            "type": "string",
            "enum": [
              "corporate_registry",
              "individual_id",
              "ownership_proof",
              "other"
            ]
          },
          "file_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "mime_type": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp",
              "image/avif",
              "image/heic",
              "application/pdf"
            ]
          },
          "file_size": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 25000000
          }
        },
        "required": [
          "doc_kind",
          "file_name",
          "mime_type",
          "file_size"
        ]
      },
      "OwnerInquiryUploadDeleteResponse": {
        "type": "object",
        "properties": {
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "asset_id": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "deleted",
          "asset_id"
        ]
      },
      "OwnerInquiryUploadSubmitResponse": {
        "type": "object",
        "properties": {
          "submitted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "inquiry_id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_count": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "email_status": {
            "type": "string",
            "enum": [
              "sent",
              "failed"
            ]
          },
          "email_error": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "submitted",
          "inquiry_id",
          "asset_count",
          "status",
          "email_status",
          "email_error"
        ]
      },
      "OwnerPasswordSetupVerifyResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "purpose": {
            "type": "string",
            "enum": [
              "invite",
              "reset"
            ]
          },
          "email": {
            "type": "string"
          },
          "expires_at": {
            "type": "string"
          }
        },
        "required": [
          "ok",
          "purpose",
          "email",
          "expires_at"
        ]
      },
      "OwnerPasswordSetupVerifyBody": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "minLength": 20,
            "maxLength": 200
          }
        },
        "required": [
          "token"
        ]
      },
      "OwnerPasswordSetupCompleteResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "purpose": {
            "type": "string",
            "enum": [
              "invite",
              "reset"
            ]
          },
          "owner_id": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "ok",
          "purpose",
          "owner_id"
        ]
      },
      "OwnerPasswordSetupCompleteBody": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "minLength": 20,
            "maxLength": 200
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          }
        },
        "required": [
          "token",
          "password"
        ]
      },
      "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"
            ]
          },
          "started_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",
          "started_at",
          "status",
          "access_count",
          "expires_at"
        ]
      },
      "WebVitalsSampleBody": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "enum": [
              "LCP",
              "INP",
              "CLS",
              "TTFB",
              "FCP"
            ],
            "description": "Core Web Vitals 指標名 (web-vitals npm の Metric.name)。",
            "examples": [
              "LCP"
            ]
          },
          "value": {
            "type": "number",
            "description": "指標値。CLS は dimensionless、それ以外はミリ秒。",
            "examples": [
              1234.5
            ]
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "web-vitals が払い出す per-metric の一意 ID。"
          },
          "navigation_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "navigate",
              "reload",
              "back-forward",
              "back-forward-cache",
              "prerender",
              "restore"
            ],
            "description": "Navigation Timing API の navigation type。"
          },
          "rating": {
            "type": "string",
            "enum": [
              "good",
              "needs-improvement",
              "poor"
            ],
            "description": "web-vitals v3+ の閾値判定 (good / needs-improvement / poor)。"
          },
          "delta_ms": {
            "type": "number",
            "description": "直前報告との差分 (ms / dimensionless)。CLS の累積などに有用。"
          },
          "url": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "計測対象ページの URL (fragment 除去済 / query は含む)。"
          },
          "ua": {
            "type": "string",
            "maxLength": 512,
            "description": "navigator.userAgent。bot filter 用。"
          },
          "sid": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "匿名セッション ID (sessionStorage 永続化の UUID v4)。ログインユーザー識別子は載せない。"
          }
        },
        "required": [
          "name",
          "value",
          "id",
          "rating",
          "delta_ms",
          "url",
          "ua",
          "sid"
        ]
      },
      "StripeWebhookAck": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "event_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processed",
              "already_processed",
              "ignored"
            ]
          }
        },
        "required": [
          "ok",
          "event_id",
          "status"
        ]
      },
      "AppleIapWebhookAck": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "notification_uuid": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processed",
              "already_processed",
              "ignored"
            ]
          }
        },
        "required": [
          "ok",
          "notification_uuid",
          "status"
        ]
      },
      "GooglePlayWebhookAck": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "message_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processed",
              "already_processed",
              "ignored"
            ]
          }
        },
        "required": [
          "ok",
          "message_id",
          "status"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/.well-known/apple-app-site-association": {
      "get": {
        "tags": [
          "well-known"
        ],
        "summary": "iOS Universal Links 設定 / iOS Universal Links Config",
        "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  または `IOS_APP_TEAM_ID` + `IOS_APP_BUNDLE_ID` を個別に指定（優先）。\n  すべて未設定時は `details` と `webcredentials.apps` を空配列にして返す。\n- 対応パス: /parking-lots/*, /parking-sessions/*, /share/parking/*, /articles/*, /auth/*",
        "responses": {
          "200": {
            "description": "Apple App Site Association",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppleAppSiteAssociation"
                }
              }
            }
          }
        },
        "operationId": "webWellKnownAppleAppSiteAssociationList"
      }
    },
    "/.well-known/assetlinks.json": {
      "get": {
        "tags": [
          "well-known"
        ],
        "summary": "Android App Links 設定 / Android App Links Config",
        "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` から取得。\n- `sha256_cert_fingerprints` は環境変数 `ANDROID_APP_CERT_FINGERPRINTS`（カンマ区切り）から取得。\n  未設定時は空配列 `[]`（Android 側は App Links 検出しないだけで 200）。",
        "responses": {
          "200": {
            "description": "Digital Asset Links",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetLinksResponse"
                }
              }
            }
          }
        },
        "operationId": "webWellKnownAssetlinksList"
      }
    },
    "/v1/codes": {
      "get": {
        "tags": [
          "コード / Codes"
        ],
        "summary": "コードマスターを一括取得 / Bulk Get Code Master",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webCodesList"
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "自分のプロフィールを取得 / Get My Profile",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webRootMe"
      },
      "patch": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "自分のプロフィールを更新 / Update My Profile",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webRootMe2"
      }
    },
    "/v1/me/withdraw": {
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "退会処理 / Withdraw Account",
        "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- サーバーは 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"
                }
              }
            }
          }
        },
        "operationId": "webMeWithdrawCreate"
      }
    },
    "/v1/me/saved-parking-lots": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "保存済み駐車場一覧 / Saved Parking Lots",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeSavedParkingLotsList"
      },
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車場を保存 / Save Parking Lot",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSavedParkingLotsCreate"
      }
    },
    "/v1/me/saved-parking-lots/{lotId}": {
      "delete": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車場の保存を解除 / Unsave Parking Lot",
        "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": "削除成功"
          }
        },
        "operationId": "webMeSavedParkingLotsDelete"
      }
    },
    "/v1/me/parking-lots/{id}/rating": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "評価 / Ratings"
        ],
        "summary": "駐車場への評価を取得 / Get My Rating",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingLotsRatingList"
      },
      "post": {
        "tags": [
          "マイページ / My Profile",
          "評価 / Ratings"
        ],
        "summary": "駐車場を評価 / Rate Parking Lot",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingLotsRatingCreate"
      },
      "delete": {
        "tags": [
          "マイページ / My Profile",
          "評価 / Ratings"
        ],
        "summary": "評価を撤回 / Withdraw Rating",
        "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 を返す）"
          }
        },
        "operationId": "webMeParkingLotsRatingDelete"
      }
    },
    "/v1/me/vehicles": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "自分の車両一覧 / My Vehicle List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeVehiclesList"
      },
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "車両を登録 / Register Vehicle",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeVehiclesCreate"
      }
    },
    "/v1/me/vehicles/{id}": {
      "patch": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "車両を更新 / Update Vehicle",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeVehiclesUpdate"
      },
      "delete": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "車両をソフト削除 / Soft Delete Vehicle",
        "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": "削除成功"
          }
        },
        "operationId": "webMeVehiclesDelete"
      }
    },
    "/v1/me/search-presets": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "検索プリセット一覧 / My Search Preset List",
        "description": "### 用途\nログイン中ユーザーが保存している **検索条件プリセット** の一覧を返す。\nプリセットは「駅から徒歩5分以内・屋根付き」「自宅周辺・月極」のように、\nよく使う検索条件を名前付きで保存した `SearchQueryV1` の JSON。\n\n### モバイルアプリでの使用タイミング\n- 検索画面で「保存した条件から選ぶ」シートを開いたとき\n- プロフィール → 検索プリセット管理画面を開いたとき\n- アプリ起動時のデフォルト条件を解決するとき（`is_default=true` を採用）\n\n### 認証\n要 Bearer JWT。サーバー側で `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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPresetsList"
      },
      "post": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "プリセットを作成 / Create Search Preset",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPresetsCreate"
      }
    },
    "/v1/me/search-presets/{id}": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "プリセット詳細 / Preset Detail",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPresetsGet"
      },
      "patch": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "プリセットを更新 / Update Search Preset",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPresetsUpdate"
      },
      "delete": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "プリセットをソフト削除 / Soft Delete Search Preset",
        "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": "削除成功"
          }
        },
        "operationId": "webMeSearchPresetsDelete"
      }
    },
    "/v1/me/search-presets/{id}/set-default": {
      "post": {
        "tags": [
          "マイページ / My Profile",
          "検索プリセット / Search Presets"
        ],
        "summary": "プリセットをデフォルトに設定 / Set Default Search Preset",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPresetsSetDefaultCreate"
      }
    },
    "/v1/me/search-preferences": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "検索設定 / Search Preferences"
        ],
        "summary": "検索ソート設定を取得 / Get Search Preferences",
        "description": "### 用途\n「総コスト順」ソートで使う「徒歩1分=N円」換算レートを返す。\n設定行が無い場合はデフォルト値 17 円/分（時給 1000 円相当）を `is_default: true` で返す。\n\n### プレミアム判定\n`user_subscriptions.status = 'active'` の行があれば `is_premium: true`。\n非プレミアムは PUT で 403 になるが、GET 自体は常に成功する。\n\n### 認証\n要 Bearer JWT。未ログイン時はクライアント側で API を呼ばずデフォルト値を使う想定。",
        "responses": {
          "200": {
            "description": "検索ソート設定",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchPreferences"
                }
              }
            }
          },
          "401": {
            "description": "未認証",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPreferencesList"
      },
      "put": {
        "tags": [
          "マイページ / My Profile",
          "検索設定 / Search Preferences"
        ],
        "summary": "検索ソート設定を更新 / Update Search Preferences",
        "description": "### 用途\n「徒歩1分=N円」換算レートを UPSERT で上書きする。\nプレミアムユーザー限定機能。非プレミアムは 403 `plan_required` を返す。\n\n### 値の範囲\n`yen_per_walk_minute` は 0〜500 の整数。0 は徒歩コスト無し（純粋な安い順）、\n値が大きいほど「近い」を重視する指標になる。\n\n### 認証\n要 Bearer JWT。自分の行のみ UPSERT。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchPreferencesUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "更新後の設定",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchPreferences"
                }
              }
            }
          },
          "401": {
            "description": "未認証",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "プレミアムプラン必須",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "バリデーションエラー",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webMeSearchPreferencesReplace"
      }
    },
    "/v1/me/parking-sessions": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "自分の駐車セッション一覧 / My Parking Sessions",
        "description": "### 用途\nログイン中ユーザー自身の駐車セッション履歴をページングして返す。\n進行中（`active`）・確定済み（`completed`）・キャンセル（`cancelled`）など\n`status` で絞り込める。`started_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- `started_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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsList"
      }
    },
    "/v1/me/parking-sessions/active": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "進行中の駐車セッションを取得 / Get Active Parking Session",
        "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"
                        },
                        {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/ParkingSessionRow"
                            },
                            {
                              "type": [
                                "object",
                                "null"
                              ],
                              "properties": {
                                "user_id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "vehicle_type": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "started_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "ended_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "total_amount_minor": {
                                  "type": [
                                    "integer",
                                    "null"
                                  ]
                                },
                                "memo": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "personal_rating": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "start_lat": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                },
                                "start_lng": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                }
                              },
                              "required": [
                                "vehicle_type",
                                "started_at",
                                "ended_at",
                                "total_amount_minor",
                                "memo",
                                "personal_rating",
                                "start_lat",
                                "start_lng"
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "required": [
                    "session"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsActiveList"
      }
    },
    "/v1/me/parking-sessions/{id}": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車セッション詳細 / Parking Session Detail",
        "description": "### 用途\n指定セッションの詳細 1 件を返す。一覧に含まれる情報を再取得する正本として使う。\n編集画面や精算画面で、最新のステータス・料金・メモ等を表示するために呼ぶ。\n\n### モバイルアプリでの使用タイミング\n- 履歴一覧から 1 件タップして詳細画面を開いたとき\n- 精算画面で最新の `total_amount_minor` / `ended_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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsGet"
      },
      "patch": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "メモ・評価を更新 / Update Note & Rating",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsUpdate"
      }
    },
    "/v1/me/parking-sessions/{id}/photos": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車セッション写真一覧 / Parking Session Photos",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsPhotosList"
      }
    },
    "/v1/me/parking-sessions/{id}/photos/{phase}/{slot}": {
      "put": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車セッション写真を登録 / Register Session Photo",
        "description": "R2 へのアップロード完了後にこのエンドポイントを呼び出し、r2_key をセッションに紐付ける。\n同一 (session_id, phase, slot) が既に存在する場合は上書き更新（ON CONFLICT DO UPDATE）。\nR2 への実際のアップロードは `POST /v1/storage/upload-url` で presigned URL を取得してから行う。\n\n### 写真ハッシュ重複検知 (T5)\n`content_hash` は SHA-256 (hex) をモバイル/Web 側で計算して送る。同一ユーザー内で重複する\nハッシュが見つかった場合、レスポンスの `is_duplicate=true` で通知される（投稿自体は可）。\n重複写真はバッジ評価・EXP ボーナスの対象外となる。",
        "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
                  },
                  "content_hash": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{64}$"
                  }
                },
                "required": [
                  "r2_key"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "登録・更新後の写真 (content_hash 重複時は is_duplicate=true)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SessionPhoto"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "is_duplicate": {
                          "type": "boolean"
                        },
                        "duplicate_of": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uuid"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "セッションが存在しないか他人の",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsPhotosReplace"
      },
      "delete": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "駐車セッション写真を削除 / Delete Session Photo",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeParkingSessionsPhotosDelete"
      }
    },
    "/v1/me/reviews": {
      "post": {
        "tags": [
          "マイページ / My Profile",
          "レビュー / Reviews"
        ],
        "summary": "レビューを投稿 / Post Review",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeReviewsCreate"
      }
    },
    "/v1/me/reviews/{id}": {
      "patch": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "レビューを編集 / Edit Review",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeReviewsUpdate"
      }
    },
    "/v1/reviews": {
      "get": {
        "tags": [
          "レビュー / Reviews"
        ],
        "summary": "公開レビュー一覧 / Public Review List",
        "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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/ReviewStatus"
                },
                {
                  "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webReviewsList"
      }
    },
    "/v1/me/notifications": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "通知一覧 / My Notification List",
        "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"
          },
          {
            "schema": {
              "type": "string",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeNotificationsList"
      }
    },
    "/v1/me/notifications/mark-read": {
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "通知を既読 / Mark Notifications as Read",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeNotificationsMarkRead"
      }
    },
    "/v1/me/notifications/types": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "通知カテゴリ一覧 / Notification Type List",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeNotificationsTypesList"
      }
    },
    "/v1/me/notifications/preferences": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "通知設定を取得 / Get Notification Preferences",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeNotificationsPreferencesList"
      },
      "put": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "通知設定を更新 / Update Notification Preferences",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeNotificationsPreferencesReplace"
      }
    },
    "/v1/me/push-tokens": {
      "put": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "デバイストークンを登録 / Register Device Token",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMePushTokensReplace"
      }
    },
    "/v1/me/exp": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "EXP・レベルを取得 / Get EXP & Level",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeExpList"
      }
    },
    "/v1/me/badges": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "獲得済みバッジ一覧 / Earned Badge List",
        "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- `GET /v1/me/badge-progress` — 未獲得バッジの進捗率\n- `GET /v1/me/exp` — 現在レベル",
        "responses": {
          "200": {
            "description": "バッジ一覧",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MyBadge"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeBadgesList"
      }
    },
    "/v1/me/badge-progress": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "バッジ進捗一覧 / Badge Progress List",
        "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\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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeBadgeProgressList"
      }
    },
    "/v1/me/themes": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "所有テーマ一覧 / My Theme List",
        "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/{theme_code}/apply` — 所有テーマを適用",
        "responses": {
          "200": {
            "description": "一覧",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MyTheme"
                  }
                }
              }
            }
          }
        },
        "operationId": "webMeThemesList"
      }
    },
    "/v1/me/themes/{theme_code}/apply": {
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "所有テーマを適用（natural key = customization_themes.theme_code）",
        "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",
              "minLength": 1,
              "maxLength": 64,
              "pattern": "^[a-z0-9_]+$"
            },
            "required": true,
            "name": "theme_code",
            "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeThemesApplyCreate"
      }
    },
    "/v1/me/subscription": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "サブスクリプション情報を取得 / Get My Subscription",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeSubscriptionList"
      }
    },
    "/v1/me/subscription/verify-iap": {
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "IAP購入レシートを検証 / Verify IAP Receipt",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeSubscriptionVerifyIapCreate"
      }
    },
    "/v1/me/subscription/refresh-iap": {
      "post": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "IAPレシートを再検証 / Refresh IAP 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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeSubscriptionRefreshIapCreate"
      }
    },
    "/v1/me/error-reports": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "誤情報報告 / Error Reports"
        ],
        "summary": "自分の通報履歴 / My Error Report History",
        "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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeErrorReportsList"
      }
    },
    "/v1/parking-lots": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "駐車場を検索 / Search Parking Lots",
        "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": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "パラメータ不正",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsList"
      }
    },
    "/v1/parking-lots/nearby": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "近傍駐車場を取得 / Get Nearby Parking Lots",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsNearbyList"
      }
    },
    "/v1/parking-lots/{id}": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "駐車場詳細 / Parking Lot Detail",
        "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"
          },
          {
            "schema": {
              "type": "string",
              "description": "検索時の最大単価（minor unit）"
            },
            "required": false,
            "name": "highlight_max_price",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "屋根あり絞込のときに `true`"
            },
            "required": false,
            "name": "highlight_roof_only",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "24時間営業絞込のときに `true`"
            },
            "required": false,
            "name": "highlight_open_24h",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "絞込タグ slug のカンマ区切り"
            },
            "required": false,
            "name": "highlight_tags",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "対応車種コード（例: sedan / kei / ev）"
            },
            "required": false,
            "name": "highlight_vehicle_type",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "駐車場詳細",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParkingLotWithDetail"
                }
              }
            }
          },
          "404": {
            "description": "存在しない",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsGet"
      }
    },
    "/v1/parking-lots/{id}/reviews": {
      "get": {
        "tags": [
          "レビュー / Reviews"
        ],
        "summary": "駐車場のレビュー一覧 / Lot Review List",
        "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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsReviewsList"
      },
      "post": {
        "tags": [
          "レビュー / Reviews"
        ],
        "summary": "レビューを投稿 / Post Review",
        "deprecated": true,
        "description": "**Deprecated**: Use `POST /v1/me/reviews` instead. Will be removed 2026-07-31.\n\nログインユーザーとして指定駐車場に対するレビュー（星評価 + 任意コメント）を投稿する。\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"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsReviewsCreate"
      }
    },
    "/v1/parking-lots/{id}/pricing-rules": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "料金ルール一覧 / Pricing Rule List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsPricingRulesList"
      }
    },
    "/v1/parking-lots/{id}/nearby-stations": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "近傍駅一覧 / Nearby Station List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsNearbyStationsList"
      }
    },
    "/v1/parking-lots/{id}/images": {
      "get": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "駐車場の画像一覧 / Parking Lot Images",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsImagesList"
      }
    },
    "/v1/parking-lots/{id}/calc-fee": {
      "post": {
        "tags": [
          "駐車場 / Parking Lots"
        ],
        "summary": "駐車料金を試算 / Calculate 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"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsCalcFeeCreate"
      }
    },
    "/v1/parking-lots/{id}/field-values/{fieldValueId}/tap": {
      "post": {
        "tags": [
          "駐車場 / Parking Lots",
          "field-taps"
        ],
        "summary": "field 値への ✓/✗ タップを記録 (web/home, 匿名 OK)",
        "description": "Phase D: web/home の駐車場詳細画面で各 field 横の「✓ 合ってる / ✗ 違う」\nボタンが叩く endpoint。匿名ユーザーで叩け、IP 単位で 60/h のレート制限がかかる。\n\nis_correct=true の場合のみ parking_field_values.confirms_n を +1 し、\npriority_score を即時再計算 (BEFORE UPDATE trigger) し、is_primary を新スコアで再判定する。\nis_correct=false の場合は監査ログのみ (admin の手動レビュー入力に使う)。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "examples": [
                "00000000-0000-0000-0000-000000000000"
              ]
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "examples": [
                "00000000-0000-0000-0000-000000000000"
              ]
            },
            "required": true,
            "name": "fieldValueId",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldTapBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "tap 記録成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FieldTapResponse"
                }
              }
            }
          },
          "400": {
            "description": "不正リクエスト",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "field_value が存在しない / lot との不一致",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "レート制限",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webParkingLotsFieldValuesTapCreate"
      }
    },
    "/v1/parking/{id}/detail": {
      "get": {
        "tags": [
          "駐車 / Parking",
          "BFF"
        ],
        "summary": "駐車場詳細（BFF集約）/ Parking Lot Detail (BFF)",
        "description": "### 用途\n駐車場詳細画面に必要なデータを 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"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "x-app-version",
            "in": "header"
          }
        ],
        "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"
          }
        },
        "operationId": "webParkingDetailList"
      }
    },
    "/v1/parking-sessions": {
      "post": {
        "tags": [
          "駐車セッション / Parking Sessions"
        ],
        "summary": "駐車開始 / Start 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": "作成された parking_sessions.id (uuid)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParkingSessionCreateResponse"
                }
              }
            }
          },
          "401": {
            "description": "未認証",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webParkingSessionsCreate"
      }
    },
    "/v1/parking-sessions/{id}/finalize": {
      "post": {
        "tags": [
          "駐車セッション / Parking Sessions"
        ],
        "summary": "駐車終了 / Finalize Parking Session",
        "description": "駐車セッションを終了し、料金を確定する。\n\n- **RPC**: `finalize_parking_session` が `ended_at` を確定、`total_amount_minor` を `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"
                  },
                  "end_lat": {
                    "type": "number"
                  },
                  "end_lng": {
                    "type": "number"
                  }
                },
                "required": [
                  "client_request_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "確定結果（session_id / total_amount_minor / ended_at / breakdown / reward）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParkingSessionFinalizeResponse"
                }
              }
            }
          }
        },
        "operationId": "webParkingSessionsFinalizeCreate"
      }
    },
    "/v1/parking-sessions/{id}/cancel": {
      "post": {
        "tags": [
          "駐車セッション / Parking Sessions"
        ],
        "summary": "駐車キャンセル / Cancel Parking Session",
        "description": "駐車セッションをキャンセルする。料金は発生しない。\n\n- **RPC**: `cancel_parking_session` がセッションを `cancelled` に遷移する。\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": "キャンセル結果（session_id / cancelled / reason）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParkingSessionCancelResponse"
                }
              }
            }
          }
        },
        "operationId": "webParkingSessionsCancel"
      }
    },
    "/v1/tags": {
      "get": {
        "tags": [
          "タグ / Tags"
        ],
        "summary": "タグ一覧（マスター）/ Tag Master List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webTagsList"
      }
    },
    "/v1/articles": {
      "get": {
        "tags": [
          "記事 / Articles"
        ],
        "summary": "公開記事一覧 / Published Article List",
        "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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webArticlesList"
      }
    },
    "/v1/articles/by-author/{slug}": {
      "get": {
        "tags": [
          "記事 / Articles"
        ],
        "summary": "著者別公開記事一覧 / Author Article List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webArticlesByAuthorGet"
      }
    },
    "/v1/articles/{slug}": {
      "get": {
        "tags": [
          "記事 / Articles"
        ],
        "summary": "記事を取得 / Get Article by 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"
                }
              }
            }
          }
        },
        "operationId": "webArticlesGet"
      }
    },
    "/v1/ads": {
      "get": {
        "tags": [
          "広告 / Ads"
        ],
        "summary": "広告一覧 / Active Ad List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webAdsList"
      }
    },
    "/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"
          }
        },
        "operationId": "webNewsletterTrackOpenGet"
      }
    },
    "/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"
          }
        },
        "operationId": "webNewsletterTrackUnsubscribe"
      }
    },
    "/v1/support/tickets": {
      "post": {
        "tags": [
          "サポート / Support"
        ],
        "summary": "サポートチケットを作成 / Create Support Ticket",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webSupportTicketsCreate"
      },
      "get": {
        "tags": [
          "サポート / Support"
        ],
        "summary": "自分のサポートチケット一覧 / My Support Tickets",
        "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",
              "description": "cursor ページング利用時の不透明トークン。offset ページング（page/limit）とは排他。"
            },
            "required": false,
            "name": "cursor",
            "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
                    },
                    "total_is_estimate": {
                      "type": "boolean",
                      "description": "true のとき total は pg_class.reltuples 由来の概算値（数千行ズレ得る）。exact COUNT のときは field 自体が省略される。"
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webSupportTicketsList"
      }
    },
    "/v1/error-reports/types": {
      "get": {
        "tags": [
          "誤情報報告 / Error Reports"
        ],
        "summary": "通報種別一覧 / Error Report Type List",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webErrorReportsTypesList"
      }
    },
    "/v1/error-reports": {
      "post": {
        "tags": [
          "誤情報報告 / Error Reports"
        ],
        "summary": "誤情報を報告 / Report Incorrect Info",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webErrorReportsCreate"
      }
    },
    "/v1/review-reports/reasons": {
      "get": {
        "tags": [
          "レビュー通報 / Review Reports"
        ],
        "summary": "レビュー通報理由一覧 / Review Report Reason List",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webReviewReportsReasonsList"
      }
    },
    "/v1/reviews/{reviewId}/flag": {
      "post": {
        "tags": [
          "レビュー / Reviews",
          "レビュー通報 / Review Reports"
        ],
        "summary": "レビューを通報 / Flag Review",
        "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- `Idempotency-Key` ヘッダ必須 (二重通報防止)\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"
                }
              }
            }
          }
        },
        "operationId": "webReviewsFlagCreate"
      }
    },
    "/v1/sponsors": {
      "get": {
        "tags": [
          "スポンサー / Sponsors"
        ],
        "summary": "スポンサー一覧 / Sponsor List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webSponsorsList"
      }
    },
    "/v1/sponsors/nearby": {
      "get": {
        "tags": [
          "スポンサー / Sponsors"
        ],
        "summary": "周辺スポンサーを取得 / Get Nearby Sponsors",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webSponsorsNearbyList"
      }
    },
    "/v1/sponsors/{id}/notification": {
      "post": {
        "tags": [
          "スポンサー / Sponsors"
        ],
        "summary": "近接通知を記録 / Record Proximity Notification",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webSponsorsNotificationCreate"
      }
    },
    "/v1/sponsors/{id}/checkin": {
      "post": {
        "tags": [
          "スポンサー / Sponsors"
        ],
        "summary": "スポンサー施設にチェックイン / Check In to Sponsor",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webSponsorsCheckinCreate"
      }
    },
    "/v1/themes": {
      "get": {
        "tags": [
          "テーマ / Themes"
        ],
        "summary": "公開テーマ一覧 / Public Theme List",
        "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/{theme_code}/apply` — 所有テーマを適用",
        "responses": {
          "200": {
            "description": "一覧",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/ThemeListItem"
                      },
                      {
                        "type": "object"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "operationId": "webThemesList"
      }
    },
    "/v1/subscription-plans": {
      "get": {
        "tags": [
          "サブスクリプション / Subscriptions"
        ],
        "summary": "購読プラン一覧 / Subscription Plan List",
        "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"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "operationId": "webSubscriptionPlansList"
      }
    },
    "/v1/storage/upload-url": {
      "post": {
        "tags": [
          "storage"
        ],
        "summary": "アップロードURLを発行 / Generate Upload 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 になる。\n- **複数ファイルを一括発行したい場合**: `POST /v1/storage/upload-urls` (複数形) で N 件まとめて取得できる。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "file_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "file_size": {
                    "type": "integer",
                    "exclusiveMinimum": 0,
                    "maximum": 52428800
                  },
                  "mime_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp",
                      "image/gif",
                      "image/avif",
                      "application/pdf"
                    ]
                  },
                  "category": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "default": "other"
                  },
                  "entity_type": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "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"
                }
              }
            }
          }
        },
        "operationId": "webStorageUploadUrlCreate"
      }
    },
    "/v1/storage/upload-urls": {
      "post": {
        "tags": [
          "storage"
        ],
        "summary": "アップロードURLを一括発行 / Batch Generate Upload URLs",
        "description": "複数ファイルを 1 リクエストで presign する batch 版。単一 `upload-url` と同じ semantics で、\n`files` 配列の順序を保って `items` を返す。\n\n### 用途\n- 駐車場登録ウィザードで 10〜50 枚の写真を一括アップロード\n- レビュー投稿で複数枚添付\n- 公開 API 往復を N → 1 に削減（各ファイル の PUT は従来通りクライアントが並列化）\n\n### 制約\n- `files` は 1〜50 件。`category` / `entity_type` / `entity_id` / `is_public` は batch 全体で共通。\n- 各ファイルの MIME / サイズは単一版と同じ allowlist / 上限（50 MiB）。\n- assets 行は multi-row INSERT で 1 回に集約（§3.2）。途中 1 件でも FK / size / mime が NG なら tx 全体 ROLLBACK（部分作成なし）。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "file_name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 300
                        },
                        "file_size": {
                          "type": "integer",
                          "exclusiveMinimum": 0,
                          "maximum": 52428800
                        },
                        "mime_type": {
                          "type": "string",
                          "enum": [
                            "image/jpeg",
                            "image/png",
                            "image/webp",
                            "image/gif",
                            "image/avif",
                            "application/pdf"
                          ]
                        }
                      },
                      "required": [
                        "file_name",
                        "file_size",
                        "mime_type"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 50,
                    "description": "アップロードするファイル群（1〜50 件）。"
                  },
                  "category": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "default": "other"
                  },
                  "entity_type": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "entity_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "examples": [
                      "00000000-0000-0000-0000-000000000000"
                    ]
                  },
                  "is_public": {
                    "type": "boolean",
                    "default": true
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "presigned URL 配列 + メタ。`items` は `files` と同じ順序。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchUploadUrlResponse"
                }
              }
            }
          }
        },
        "operationId": "webStorageUploadUrlsCreate"
      }
    },
    "/v1/storage/assets/{id}/finalize": {
      "post": {
        "tags": [
          "storage"
        ],
        "summary": "アップロード完了を確定 / Finalize Upload",
        "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"
                    ],
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "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"
                }
              }
            }
          }
        },
        "operationId": "webStorageAssetsFinalizeCreate"
      }
    },
    "/v1/search/lots": {
      "get": {
        "tags": [
          "駐車場検索データ / Search Data"
        ],
        "summary": "駐車場データを一括取得（SSG用）/ Bulk Get Lots for Search (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\n### モバイルアプリでの使用タイミング\n- 原則モバイルからは叩かない（ペイロードが重いので検索は `/v1/parking-lots` 系を使う）\n- Astro SSG（`web/home`）のビルド時 1 回 + 管理画面の全量エクスポートがメインユーザー\n\n### 認証\n不要（optionalUser）。公開データのみ返す。\n\n### 挙動・制約\n- 並び順: `name` 昇順（geo 未指定時）/ ST_Distance 昇順（geo 指定時）\n- `limit` は 1〜30000、省略時 5000（geo 指定時の default は 200）。超過値はバリデーションで 400\n- `since` 指定時はキャッシュを付与しない（差分なので毎回フレッシュに取得する必要がある）\n- キャッシュ（`since` 未指定時）: ブラウザ 60 秒 / エッジ 5 分\n- geo（lat / lng / radius_m）指定時はキャッシュ強め（`public, max-age=300, s-maxage=600`）\n\n### geo フィルタ (lat / lng / radius_m)\n- 3 つ全部指定すると半径 radius_m メートル以内の lot のみを距離昇順で返す\n- 部分指定（例: lat だけ）は 400 bad_request\n- 想定ユーザー: SSR 化された /search ページ（URL クエリ → BFF → JSON）\n\n### 関連\n- `GET /v1/parking-lots` — 普段の検索・ページング用\n- `GET /v1/hubs/{stationId}/parking-lots` — 駅ハブ近傍ダンプ",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "description": "最大件数（デフォルト 5000、geo 指定時のみ 200）",
              "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"
          },
          {
            "schema": {
              "type": "string",
              "description": "中心点の緯度（-90..90）。lng / radius_m と必ずセット",
              "examples": [
                "35.6586"
              ]
            },
            "required": false,
            "name": "lat",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "中心点の経度（-180..180）。lat / radius_m と必ずセット",
              "examples": [
                "139.7454"
              ]
            },
            "required": false,
            "name": "lng",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "検索半径（m、1..50000）。lat / lng と必ずセット",
              "examples": [
                "3000"
              ]
            },
            "required": false,
            "name": "radius_m",
            "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",
                      "description": "次回リクエストで since に渡す値。現状は server_time と同値。",
                      "examples": [
                        "2026-04-21T06:00:00.000Z"
                      ]
                    }
                  },
                  "required": [
                    "items",
                    "page",
                    "limit",
                    "total",
                    "server_time",
                    "cursor"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webSearchLotsList"
      }
    },
    "/v1/search/ai": {
      "post": {
        "tags": [
          "検索 / Search"
        ],
        "summary": "AI検索クエリをパース / Parse AI Search Query",
        "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 単位にレートリミットを適用。超過時 429。上限値は security.search_rate_limit_max_hits に合わせること。\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"
                }
              }
            }
          }
        },
        "operationId": "webSearchAiCreate"
      }
    },
    "/v1/hubs/publishable": {
      "get": {
        "tags": [
          "ハブ / Hubs"
        ],
        "summary": "公開ハブ一覧 / Publishable Hub List",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webHubsPublishableList"
      }
    },
    "/v1/hubs/by-slug/{prefSlug}/{citySlug}/{spotSlug}": {
      "get": {
        "tags": [
          "ハブ / Hubs"
        ],
        "summary": "駅ハブを取得 / Get Hub by 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": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webHubsBySlugGet"
      }
    },
    "/v1/hubs/{stationId}/parking-lots": {
      "get": {
        "tags": [
          "ハブ / Hubs"
        ],
        "summary": "駅ハブ周辺の駐車場一覧 / Hub Nearby Parking Lots",
        "description": "### 用途\n指定駅（`station_id`）の近傍に登録されている駐車場を、距離で近い順にまとめて返す。\n各駐車場に `pricing_rules` と `tags`、寸法制約、運営会社名までフラットに同梱するので、\n駅ハブページ側はこれ 1 本でカード列 + フィルタまで描画できる。\n\n### 実装\n内部的に駅の lat/lng を解決して `/v1/hubs/parking-lots/nearby` と同じ data 層関数\n`webListNearbyParkingLots` (PostGIS ST_DWithin + ST_Distance) を呼ぶ。\n旧 `parking_lot_nearby_spots` ベース SQL は廃止。\n\n### 認証\n不要（optionalUser）。公開データのみ。\n\n### 挙動・制約\n- 並び順: ST_Distance (m) 昇順\n- `walk_min` は 80m/分換算 (CEIL)\n- 半径は 1000m / 最大 50 件 (駅ハブ既存挙動を踏襲)\n- 該当なしの駅 ID でも空配列を返す（404 は返さない）\n- キャッシュ: ブラウザ 5 分 / エッジ 10 分",
        "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"
                  }
                }
              }
            }
          }
        },
        "operationId": "webHubsParkingLotsList"
      }
    },
    "/v1/hubs/parking-lots/nearby": {
      "get": {
        "tags": [
          "ハブ / Hubs"
        ],
        "summary": "任意座標近傍の駐車場（距離昇順, 駅ハブと同 payload）",
        "description": "### 用途\n任意の lat/lng (観光地・空港・イベント施設など) から近い順に駐車場をまとめて返す。\nレスポンス shape は `/v1/hubs/{stationId}/parking-lots` と完全同一なので、\nランドマーク個別ページが駅ハブと同じ adapter / UI で描画できる。\n\n### パラメータ\n- `lat`, `lng` (required): 緯度経度 (10進)\n- `radius_m` (optional): 半径メートル。default 2000\n- `limit` (optional): 最大件数。default 30\n\n### 認証\n不要 (optionalUser)。公開データのみ。\n\n### キャッシュ\nブラウザ 5 分 / エッジ 10 分。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "examples": [
                "35.6586"
              ]
            },
            "required": true,
            "name": "lat",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "examples": [
                "139.7454"
              ]
            },
            "required": true,
            "name": "lng",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "examples": [
                "2000"
              ]
            },
            "required": false,
            "name": "radius_m",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "examples": [
                "30"
              ]
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "駐車場一覧",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/HubParkingLotItem"
                  }
                }
              }
            }
          }
        },
        "operationId": "webHubsParkingLotsNearbyList"
      }
    },
    "/v1/meta/activity-types": {
      "get": {
        "tags": [
          "メタ情報 / Meta Info"
        ],
        "summary": "アクティビティ定義一覧 / Activity Type Definitions",
        "description": "サーバー側が `award_user_activity` に emit する全 activity_type のカタログ。\n\n- 各エントリに JSON Schema 7 形式の `metadata_schema` を含む。\n- `emitted=false` は seed には定義されているがサーバー側で emit が未配線の予約済み種別。\n- SSoT: `api/src/lib/activity-types.ts`。新しい種別を足したら当エンドポイントの出力にも自動反映される。",
        "responses": {
          "200": {
            "description": "カタログ全件",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityTypesResponse"
                }
              }
            }
          }
        },
        "operationId": "webMetaActivityTypesList"
      }
    },
    "/v1/meta/deep-links": {
      "get": {
        "tags": [
          "メタ情報 / Meta Info"
        ],
        "summary": "ディープリンクルーティングテーブル / Deep Link Routing Table",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMetaDeepLinksList"
      }
    },
    "/v1/meta/app": {
      "get": {
        "tags": [
          "メタ情報 / Meta Info"
        ],
        "summary": "アプリ設定情報を取得 / Get App Meta Config",
        "description": "モバイルアプリ起動時に必ず叩くエンドポイント。\n- `is_maintenance: true` のとき、アプリはメンテナンス画面を表示する\n- `min_app_version_*` より現在のバージョンが古ければ強制アップデート画面を表示する\n- 設定が未登録の場合はデフォルト値を返す（エラーにしない）",
        "responses": {
          "200": {
            "description": "アプリ設定",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppConfig"
                }
              }
            }
          }
        },
        "operationId": "webMetaAppList"
      }
    },
    "/v1/jp-holidays": {
      "get": {
        "tags": [
          "メタ情報 / Meta Info"
        ],
        "summary": "日本の祝日一覧",
        "description": "日本の祝日（国民の祝日・振替休日・国民の休日）を返す。\n`year` を指定すればその年の祝日一覧、`from`/`to` を指定すれば範囲で取得する。\nどちらも省略時は当年の祝日を返す。\n\n公開情報のため認証不要。Cache-Control は 1 日。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}$"
            },
            "required": false,
            "name": "year",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "required": false,
            "name": "from",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "required": false,
            "name": "to",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "祝日一覧",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JpHoliday"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webJpHolidaysList"
      }
    },
    "/v1/me/data-export": {
      "get": {
        "tags": [
          "マイページ / My Profile"
        ],
        "summary": "個人データをエクスポート / Export Personal Data",
        "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_parkings 全件）\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"
                }
              }
            }
          }
        },
        "operationId": "webMeDataExportList"
      }
    },
    "/v1/me/referrals/my-code": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "紹介 / Referrals"
        ],
        "summary": "紹介コードを取得 / Get My Referral Code",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeReferralsMyCodeList"
      }
    },
    "/v1/me/referrals/apply": {
      "post": {
        "tags": [
          "マイページ / My Profile",
          "紹介 / Referrals"
        ],
        "summary": "招待コードを適用 / Apply Referral Code",
        "description": "### 用途\n他ユーザーの紹介コードを入力し、紹介関係を登録する。\n1 ユーザーは 1 回のみ適用可能（`referee_user_id` に UNIQUE 制約）。\n\n### 遅延確定 (T4 対策)\n- 適用直後は `status='pending'` で記録。EXP は付与されない。\n- 被紹介者が初回駐車を完了し、累計 500 円以上で finalize_parking_session 内の\n  `confirm_pending_referrals_for_user` により確定 → 両者に EXP 付与。\n- アカウント作成から **24 時間以内** のみ適用可能。\n- `device_fingerprint` が紹介元と一致 or 他ユーザーで既出なら `duplicate_device` で拒否。\n\n### エラーコード（ok=false 時）\n| error | 意味 |\n|---|---|\n| `already_referred` | 既に紹介を受けている |\n| `code_not_found` | コードが存在しない or 無効化済み |\n| `self_referral` | 自分のコードを使用しようとした |\n| `code_exhausted` | コードの利用上限に達した |\n| `window_expired` | アカウント作成から 24h 超過 |\n| `duplicate_device` | 紹介元と同一 device_fingerprint（複垢検知） |\n| `referee_not_found` | 被紹介者が見つからない |\n\n### 認証\n要 Bearer JWT。`apply_user_referral_v2` RPC に委譲。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 16
                  },
                  "device_fingerprint": {
                    "type": "string",
                    "minLength": 4,
                    "maxLength": 256
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "適用成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplyReferralResponse"
                }
              }
            }
          },
          "400": {
            "description": "コードが無効 / 既に適用済み / 自己紹介 / 上限到達",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplyReferralResponse"
                }
              }
            }
          }
        },
        "operationId": "webMeReferralsApplyCreate"
      }
    },
    "/v1/me/referrals/history": {
      "get": {
        "tags": [
          "マイページ / My Profile",
          "紹介 / Referrals"
        ],
        "summary": "紹介履歴一覧 / Referral History",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeReferralsHistoryList"
      }
    },
    "/v1/me/consents/types": {
      "get": {
        "tags": [
          "同意管理 / Consents"
        ],
        "summary": "同意タイプ一覧 / Consent Type List",
        "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"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webMeConsentsTypesList"
      }
    },
    "/v1/me/consents": {
      "get": {
        "tags": [
          "同意管理 / Consents"
        ],
        "summary": "同意状況を取得 / Get My Consents",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeConsentsList"
      },
      "post": {
        "tags": [
          "同意管理 / Consents"
        ],
        "summary": "同意を一括更新 / Bulk Update Consents",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeConsentsCreate"
      }
    },
    "/v1/me/consents/status": {
      "get": {
        "tags": [
          "同意管理 / Consents"
        ],
        "summary": "未承諾の必須同意一覧 / Pending Required Consents",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webMeConsentsStatusList"
      }
    },
    "/v1/me/device-permissions": {
      "put": {
        "tags": [
          "デバイス権限 / Device Permissions"
        ],
        "summary": "デバイス権限を更新 / Update Device Permissions",
        "description": "### 用途\nOS が返す location permission / push notification permission の現在値を\nサーバーに同期する。クライアントは下記のタイミングで呼ぶ:\n- オンボーディング後のパーミッション要求完了時\n- アプリ起動時（OS 設定画面で変更された可能性があるため）\n- 駐車開始時の `locationAlways` 昇格後\n\n### `user_consents` との違い\n- `user_device_permissions` = **OS ネイティブ許可の事実**（granted / denied / not_determined）\n- `user_consents` = **ユーザーの同意の意思**（Parky 利用規約への同意）\n- 両者は独立に記録する。モバイルクライアントは通常、両方の API を呼ぶ。\n\n### UPSERT 仕様\n同一 `(user_id, device_id)` は UPDATE で上書き。\n\n### 認証\n要 Bearer JWT。自分のデバイスのみ書き込める（RLS `udp_self_rw`）。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevicePermissionsBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "登録・更新成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevicePermissionsResponse"
                }
              }
            }
          },
          "401": {
            "description": "未認証",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "バリデーションエラー",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webMeDevicePermissionsReplace"
      }
    },
    "/v1/auth/config": {
      "get": {
        "tags": [
          "認証 / Auth"
        ],
        "summary": "認証設定を取得 / Get Auth Config",
        "description": "### 用途\nモバイルクライアント起動時に 1 回 pull し、以下を取得する:\n- パスワードポリシー（minLength / 要件 / ローカライズメッセージ）\n- 有効な OAuth プロバイダ一覧\n- OTP TTL / 再送クールダウン / 試行上限\n- Lockout 閾値と継続時間\n\n### 用途の例\n- サインアップ画面のパスワード入力インラインバリデーション\n- OAuth ボタンの表示/非表示\n- OTP カウントダウンタイマー初期値\n- ロックアウト UI のメッセージ\n\n### 認証\n不要（匿名でも取得可能）。",
        "responses": {
          "200": {
            "description": "認証設定",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthConfig"
                }
              }
            }
          }
        },
        "operationId": "webAuthConfigList"
      }
    },
    "/v1/auth/preflight": {
      "post": {
        "tags": [
          "認証 / Auth"
        ],
        "summary": "メール登録状態を確認 / Check Email Registration",
        "description": "### 用途\nサインアップ/サインイン前に email の状態を判定する:\n- `available` — 新規登録可能\n- `exists_with_password` — email+password で登録済（サインイン誘導）\n- `exists_with_oauth` — OAuth プロバイダで登録済（provider を返す）\n- `withdrawn_rejoinable` — 退会済だが再登録可能\n- `blocked` — 利用停止中（再登録不可）\n\n### セキュリティ\n- Rate limit: 10/60s (signup scope)\n- Timing attack 緩和: 応答時間を概ね一定化（80ms ベース）\n\n### 認証\n不要。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PreflightBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "email 状態判定結果",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreflightResponse"
                }
              }
            }
          },
          "429": {
            "description": "レート制限超過",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webAuthPreflightCreate"
      }
    },
    "/v1/auth/login-failure": {
      "post": {
        "tags": [
          "認証 / Auth"
        ],
        "summary": "ログイン失敗を報告 / Report Login Failure",
        "description": "### 用途\nクライアント (Supabase Auth 直呼び) がログイン失敗を検知したときに呼び出す。\nサーバーは email 単位で失敗カウントを保持し、段階的に `locked_until` を返す:\n- 5 回失敗 → 15 分ロック\n- 10 回失敗 → 1 時間ロック\n- 15 回失敗 → 24 時間ロック\n\n### 返り値\n- `count`: 現在の連続失敗数\n- `locked_until`: ロック解除時刻（ロック中のみ非 null）\n\n### 認証\n不要。Rate limit 5/60s で abuse 防止。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginResultBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "失敗カウンタ更新結果",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginFailureResponse"
                }
              }
            }
          },
          "429": {
            "description": "レート制限超過",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webAuthLoginFailureCreate"
      }
    },
    "/v1/auth/login-success": {
      "post": {
        "tags": [
          "認証 / Auth"
        ],
        "summary": "ログイン成功を報告 / Report Login Success",
        "description": "### 用途\nクライアントがログイン成功を検知したときに呼び出す。\nemail 単位の soft-lock カウンタを 0 にリセットする。\n\n### 認証\n不要（email は被認証者のもの。悪意の reset は既にログイン成功した者にのみ意味あり）。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginResultBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "リセット完了",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginSuccessResponse"
                }
              }
            }
          }
        },
        "operationId": "webAuthLoginSuccessCreate"
      }
    },
    "/v1/client-events": {
      "post": {
        "tags": [
          "クライアントイベント / Client Events"
        ],
        "summary": "クライアントイベントを送信 / Send Client Event",
        "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を `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
                  },
                  "device_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"
                }
              }
            }
          }
        },
        "operationId": "webClientEventsCreate"
      }
    },
    "/v1/web/owner-inquiries": {
      "post": {
        "tags": [
          "owner-inquiries"
        ],
        "summary": "公開 LP からのオーナー掲載申込を受け付け",
        "description": "### 用途\n公開 LP（/for-owners/）の申込フォームから送信される「オーナー掲載申込」を\n`public.owner_inquiries` テーブルに 1 行 INSERT する。\n\n### 認証\n不要（未認証ユーザーからの送信を許容する）。\n悪用対策として `RATE_LIMIT_USER` による IP 単位のソフトリミットを掛ける。\n\n### べき等性\n`Idempotency-Key` 必須（routes-manifest `idempotent: true`）。\n加えてサーバー側でも直近 60 分以内の同 (contact_email + lot_name + lot_address)\n重複を検知し、既存 id + `deduplicated: true` を返す。\n\n### 運用\n- 受付後、事務局が admin portal の「オーナー申込」画面で内容確認 → 書類提出 URL をメール送付\n- 書類確認後、事務局がオーナーアカウントを発行し初期パスワードを通知\n- 本 endpoint は公開フォームで機微な書類（謄本・身分証等）を受け取らない設計",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnerInquirySubmitBody"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "申込受付成功。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerInquiryCreated"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "unprocessable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebOwnerInquiriesCreate"
      }
    },
    "/v1/web/owner-inquiry-uploads/{token}": {
      "get": {
        "tags": [
          "owner-inquiry-uploads"
        ],
        "summary": "書類アップロード URL の有効性確認",
        "description": "専用 URL ページ（/for-owners/upload/{token}/）が開いたときに、token の有効性と申込のサマリを取得するための公開 endpoint。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 32,
              "maxLength": 128
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "有効",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerInquiryUploadTokenSummary"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebOwnerInquiryUploadsGet"
      }
    },
    "/v1/web/owner-inquiry-uploads/{token}/upload-url": {
      "post": {
        "tags": [
          "owner-inquiry-uploads"
        ],
        "summary": "R2 presigned PUT URL を発行 + 書類リンク作成",
        "description": "token 有効性を検証 → assets 行作成 + presigned PUT URL 返却 + owner_inquiry_assets リンク作成。クライアントは返却された URL に直接 PUT してファイル本体を R2 にアップロードする。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 32,
              "maxLength": 128
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnerInquiryUploadUrlBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "発行成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerInquiryUploadUrlResponse"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebOwnerInquiryUploadsUploadUrlCreate"
      }
    },
    "/v1/web/owner-inquiry-uploads/{token}/assets/{asset_id}": {
      "delete": {
        "tags": [
          "owner-inquiry-uploads"
        ],
        "summary": "アップロード済み書類を削除 / Delete Uploaded Document",
        "description": "アップロードした書類 1 件を取り消す。token が有効（invalidated_at IS NULL かつ expires_at > NOW()）の間だけ実行可能。owner_inquiry_assets リンク + assets 行 + R2 オブジェクトを削除する（R2 削除は best-effort で失敗しても 200）。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 32,
              "maxLength": 128
            },
            "required": true,
            "name": "token",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "asset_id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "削除成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerInquiryUploadDeleteResponse"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebOwnerInquiryUploadsAssetsDelete"
      }
    },
    "/v1/web/owner-inquiry-uploads/{token}/submit": {
      "post": {
        "tags": [
          "owner-inquiry-uploads"
        ],
        "summary": "アップロード書類を最終提出 / Submit Uploaded Documents",
        "description": "申請者が確認画面で Confirm を押した時に呼ばれる。owner_inquiries.status を documents_submitted に遷移させ、トークンを invalidate して、申請者に受領メールを送信する。",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 32,
              "maxLength": 128
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "提出成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerInquiryUploadSubmitResponse"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebOwnerInquiryUploadsSubmit"
      }
    },
    "/v1/owner-public/password-setup/verify": {
      "post": {
        "tags": [
          "owner-password-setup"
        ],
        "summary": "パスワード設定リンクのトークンを検証",
        "description": "### 用途\nOwner Portal の `/password-setup` ページが初期表示時に呼ぶ。トークンの\n有効性を確認し、purpose（招待 / リセット）と送信先メールアドレスを返す。\nここで OK を確認できた場合のみ、UI はパスワード入力フォームを表示する。\n\n### 認証\n不要。トークンそのものが使い捨ての認可を持つ。\n\n### 挙動・制約\n- 期限切れ / 使用済み / 無効化済みはすべて 410 で同等のメッセージ\n- IP 単位のレート制限を `RATE_LIMIT_USER` 経由で軽くかける\n- レスポンスはメール本文と一致するメールアドレスを返す（UI で確認のため）",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnerPasswordSetupVerifyBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "トークン有効",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerPasswordSetupVerifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "gone (token expired / consumed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webOwnerPublicPasswordSetupVerify"
      }
    },
    "/v1/owner-public/password-setup/complete": {
      "post": {
        "tags": [
          "owner-password-setup"
        ],
        "summary": "パスワード設定リンクを消費して新パスワードを設定",
        "description": "### 用途\nOwner Portal の `/password-setup` ページのフォーム送信。トークンを再検証し、\nSupabase Auth のパスワードを更新したうえで、トークンを使用済みに更新する。\npurpose='invite' の場合は owners.status を 'active' に冪等遷移する。\n\n### 認証\n不要。トークンそのものが使い捨ての認可を持つ。\n\n### 挙動・制約\n- /verify と同じ失効判定（404/410 同等）\n- Supabase Auth password 更新失敗は 500 で返し、token は未消費のまま残す\n- 成功時は token.used_at = NOW() で再利用不可化\n- 通常は CSRF 対策として Idempotency-Key を routes-manifest 側で必須化（idempotent: true）",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OwnerPasswordSetupCompleteBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "パスワード設定完了",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerPasswordSetupCompleteResponse"
                }
              }
            }
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "gone (token expired / consumed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "unprocessable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "too_many_requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webOwnerPublicPasswordSetupComplete"
      }
    },
    "/v1/shares/{token}": {
      "get": {
        "tags": [
          "共有 / Shares"
        ],
        "summary": "共有トークンで駐車情報を取得 / Get Shared Parking Location",
        "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"
                }
              }
            }
          }
        },
        "operationId": "webSharesGet"
      }
    },
    "/v1/web/vitals": {
      "post": {
        "tags": [
          "web-vitals"
        ],
        "summary": "Web Vitals RUM サンプルを受信",
        "description": "### 用途\n公開サイト (parky.co.jp / dev.parky.co.jp) のブラウザクライアントから\nCore Web Vitals (LCP / INP / CLS / TTFB / FCP) を 1 datapoint ずつ受信し、\nCloudflare Analytics Engine (`WEB_VITALS_AE` binding) に書き出す。\n\n### 認証\n不要 (anonymous RUM beacon)。匿名 sid (sessionStorage UUID) を user 識別子代わりに使う。\n\n### レート制限\n`RATE_LIMIT_USER` binding がある環境では IP 単位でソフト制限。\n超過時は 204 でサイレント drop (RUM が落ちて UX が壊れないように)。\n\n### 集計\n受信したデータは Cloudflare Analytics Engine SQL API でクエリ。\n詳細は docs/ops/web-vitals-rum.md 参照。",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebVitalsSampleBody"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "受信完了 (body なし)。rate limit 超過時もここに含まれる。"
          },
          "400": {
            "description": "validation_error / bad_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "unprocessable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "webWebVitalsCreate"
      }
    },
    "/v1/webhooks/stripe": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Stripe webhook handler",
        "responses": {
          "200": {
            "description": "Webhook acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StripeWebhookAck"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "request_id": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "request_id"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Stripe is not configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "request_id": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "request_id"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webWebhooksStripeCreate"
      }
    },
    "/v1/webhooks/apple-iap": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Apple App Store Server Notifications V2",
        "description": "### 用途\nApple App Store Server Notifications V2 を受け取り、`subscription_events` に記録し\n`user_subscriptions` のステータスを同期する。\n\n### 認証・認可\nJWT 認証なし。Apple が送信する JWS signedPayload の base64url デコードで検証する。\n\n### 冪等性\n- L1: `subscription_events(payment_provider, external_event_id)` UNIQUE で同 notificationUUID を弾く\n- L2: `updateSubscriptionStatus` は冪等な SET\n\n### 注意\nApple は 4xx / 5xx で再送するため、ユーザー不明時も 200 を返す。",
        "responses": {
          "200": {
            "description": "受信 ACK（常に 200）",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppleIapWebhookAck"
                }
              }
            }
          }
        },
        "operationId": "webWebhooksAppleIapCreate"
      }
    },
    "/v1/webhooks/google-play": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Google Play Real-Time Developer Notifications (Pub/Sub)",
        "description": "### 用途\nGoogle Cloud Pub/Sub push サブスクリプション経由で DeveloperNotification を受け取り、\n`subscription_events` に記録し `user_subscriptions` のステータスを同期する。\n\n### 認証・認可\nJWT 認証なし。`Authorization: Bearer <jwt>` の iss / aud を検証する。\n\n### 冪等性\n- L1: `subscription_events(payment_provider, external_event_id)` UNIQUE で同 messageId を弾く\n- L2: `updateSubscriptionStatus` は冪等な SET\n\n### 注意\nGoogle Pub/Sub は 2xx 以外で再送するため、ユーザー不明時も 200 を返す。",
        "responses": {
          "200": {
            "description": "受信 ACK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GooglePlayWebhookAck"
                }
              }
            }
          },
          "401": {
            "description": "Authorization 欠落・audience 未設定・JWT 検証失敗",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        },
                        "request_id": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "request_id"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        },
        "operationId": "webWebhooksGooglePlayCreate"
      }
    }
  },
  "webhooks": {}
}
