{"openapi":"3.1.0","info":{"title":"Parky API (Marketing Portal)","version":"0.1.0","description":"## 日本語\nParky BFF — Cloudflare Workers 上で動作する `/v1/*` API。モバイル・管理ポータル・公開 Web のすべてのクライアントがこの API のみを叩く。\n\n各 operation には想定呼出し元（portal-admin / portal-owner / app-web / app-mobile）を `x-channels` に保持し、Redoc ではバッジとしてタイトル横に表示される。ドキュメント用メタデータであり、サーバー側の振る舞いは呼出し元に依存しない。\n\n## English\nParky BFF — the `/v1/*` API running on Cloudflare Workers. Every client (mobile, admin portal, public web) hits only this API.\n\nEach operation declares its intended callers (portal-admin / portal-owner / app-web / app-mobile) via `x-channels`, rendered by Redoc as colored badges next to the operation title. This is documentation-only metadata; server behavior does not branch on the caller.\n\n> Route summaries / descriptions are primarily in Japanese. An English overlay is applied when the docs page language is switched to EN — see `docs/i18n/openapi-en-overlay.json` for current coverage."},"servers":[{"url":"https://dev-api.parky.co.jp","description":"Dev"},{"url":"https://api.parky.co.jp","description":"Production"},{"url":"http://localhost:8787","description":"Local"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase Auth が発行した JWT（HS256 / SUPABASE_JWT_SECRET 署名）"}},"schemas":{"AppleAppSiteAssociation":{"type":"object","properties":{"applinks":{"type":"object","properties":{"details":{"type":"array","items":{"type":"object","properties":{"appIDs":{"type":"array","items":{"type":"string"}},"components":{"type":"array","items":{"type":"object","properties":{"/":{"type":"string"},"comment":{"type":"string"}},"required":["/"]}}},"required":["appIDs","components"]}}},"required":["details"]},"webcredentials":{"type":"object","properties":{"apps":{"type":"array","items":{"type":"string"}}},"required":["apps"]}},"required":["applinks","webcredentials"]},"AssetLinksEntry":{"type":"object","properties":{"relation":{"type":"array","items":{"type":"string"}},"target":{"type":"object","properties":{"namespace":{"type":"string"},"package_name":{"type":"string"},"sha256_cert_fingerprints":{"type":"array","items":{"type":"string"}}},"required":["namespace","package_name","sha256_cert_fingerprints"]}},"required":["relation","target"]},"AssetLinksResponse":{"type":"array","items":{"$ref":"#/components/schemas/AssetLinksEntry"}},"CodeRow":{"type":"object","properties":{"category_id":{"type":"string"},"code":{"type":"string"},"display_label":{"type":"string"},"lang":{"type":"string"},"sort_order":{"type":"integer"},"metadata":{"type":["object","null"],"additionalProperties":{}}},"required":["category_id","code","display_label","lang","sort_order","metadata"]},"CodesResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CodeRow"}},"lang":{"type":"string"}},"required":["items","lang"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","examples":["not_found"]},"message":{"type":"string","examples":["Not Found"]},"request_id":{"type":"string","examples":["7d4e5…-…"]}},"required":["code","message","request_id"]}},"required":["error"]},"Me":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid"},"email":{"type":["string","null"]},"app_user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","display_name","status","created_at"]},"admin":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"status":{"type":"string"}},"required":["id","name","status"]}},"required":["user_id","email","app_user","admin"]},"MeUpdate":{"type":"object","properties":{"display_name":{"type":"string","minLength":1,"maxLength":100}}},"SavedParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","created_at"]},"ParkingLotRating":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"value":{"type":"string","enum":["good","bad"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","value","created_at","updated_at"]},"UserVehicle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"vehicle_type":{"type":"string"},"nickname":{"type":["string","null"]},"plate_number":{"type":["string","null"]},"color":{"type":["string","null"]},"is_default":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","user_id","vehicle_type","nickname","plate_number","color","is_default","created_at"]},"SearchQueryV1":{"type":"object","properties":{"v":{"type":"number","enum":[1],"description":"スキーマバージョン","examples":[1]},"center":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"placeName":{"type":"string","maxLength":200}},"required":["lat","lng"]},"radius_m":{"type":"integer","exclusiveMinimum":0,"maximum":50000},"price_min":{"type":["integer","null"],"minimum":0,"maximum":100000},"price_max":{"type":["integer","null"],"minimum":0,"maximum":100000},"attributes":{"type":"array","items":{"type":"string","enum":["covered","open_24h","entry_24h","ev_charging","oversized_ok","motorcycle_ok","wheelchair_accessible","barrier_free","reservable","security_camera","has_max_fee","monthly_available","coin_500_or_less","near_station","low_price","partner_facility","24h","max_fee"]},"maxItems":20},"difficulty":{"type":"array","items":{"type":"string","enum":["easy","normal","hard"]},"maxItems":3},"operator_codes":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":50},"vehicle":{"type":"object","properties":{"height_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":10},"width_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":5},"length_m":{"type":["number","null"],"exclusiveMinimum":0,"maximum":20},"weight_t":{"type":["number","null"],"exclusiveMinimum":0,"maximum":50},"clearance_cm":{"type":["integer","null"],"minimum":0,"maximum":100},"tire_width_mm":{"type":["integer","null"],"exclusiveMinimum":0,"maximum":500}}},"keywords":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":10},"meter_ticket_included":{"type":"boolean"},"sort":{"type":"string","enum":["distance","price","recommended"]}},"additionalProperties":false},"SearchPreset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"is_default":{"type":"boolean"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","name","query_json","is_default","sort_order","created_at","updated_at"]},"ParkingSession":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"vehicle_type":{"type":["string","null"]},"status":{"type":"string"},"start_at":{"type":["string","null"]},"end_at":{"type":["string","null"]},"fee_amount":{"type":["integer","null"]},"memo":{"type":["string","null"]},"personal_rating":{"type":["string","null"]},"start_lat":{"type":["number","null"]},"start_lng":{"type":["number","null"]}},"required":["id","user_id","parking_lot_id","vehicle_type","status","start_at","end_at","fee_amount","memo","personal_rating","start_lat","start_lng"]},"ParkingSessionDetail":{"allOf":[{"$ref":"#/components/schemas/ParkingSession"},{"type":"object","properties":{"photos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["during","after"]},"slot":{"type":"integer"},"r2_key":{"type":"string"},"content_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"created_at":{"type":"string"}},"required":["id","phase","slot","r2_key","content_type","size_bytes","created_at"]}}},"required":["photos"]}]},"SessionPhoto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["during","after"]},"slot":{"type":"integer","minimum":1,"maximum":4},"r2_key":{"type":"string"},"content_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"created_at":{"type":"string"}},"required":["id","phase","slot","r2_key","content_type","size_bytes","created_at"]},"MyReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","parking_lot_id","user_id","rating","comment","status","created_at"]},"PublicReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_name":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","parking_lot_id","user_id","user_name","rating","comment","status","created_at"]},"UserNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"type":{"type":"string"},"target":{"type":"string"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","title","body","type","target","status","scheduled_at","sent_at","created_at"]},"NotifType":{"type":"object","properties":{"code":{"type":"string"},"display_label":{"type":"string"}},"required":["code","display_label"]},"NotifPrefItem":{"type":"object","properties":{"notif_type":{"type":"string"},"push_enabled":{"type":"boolean"},"in_app_enabled":{"type":"boolean"},"email_enabled":{"type":"boolean"}},"required":["notif_type","push_enabled","in_app_enabled","email_enabled"]},"UserPushToken":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"fcm_token":{"type":"string"},"device_type":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","fcm_token","device_type","created_at","updated_at"]},"MyExp":{"type":"object","properties":{"total_exp":{"type":"integer"},"level":{"type":"integer"},"next_level_exp":{"type":["integer","null"]},"exp_to_next_level":{"type":["integer","null"]}},"required":["total_exp","level","next_level_exp","exp_to_next_level"]},"MyBadge":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"badge_id":{"type":"string","format":"uuid"},"earned_at":{"type":"string"},"badge":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"category":{"type":"string"}},"required":["id","name","description","icon","category"]}},"required":["id","badge_id","earned_at","badge"]},"MyBadgeProgress":{"type":"object","properties":{"badge_id":{"type":"string","format":"uuid"},"count":{"type":"integer"},"threshold":{"type":"integer"},"percent":{"type":"number"},"badge":{"type":["object","null"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"category":{"type":"string"}},"required":["name","description","icon","category"]}},"required":["badge_id","count","threshold","percent","badge"]},"ThemeListItem":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"is_free":{"type":"boolean"},"price_yen_minor":{"type":["integer","null"]},"preview_asset_id":{"type":["string","null"],"format":"uuid"},"sort_order":{"type":"integer"}},"required":["id","name","description","is_free","price_yen_minor","preview_asset_id","sort_order"]},"MyTheme":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"theme_id":{"type":"string","format":"uuid"},"acquired_at":{"type":"string"},"acquisition_type":{"type":"string"},"theme":{"$ref":"#/components/schemas/ThemeListItem"}},"required":["id","theme_id","acquired_at","acquisition_type","theme"]},"SubscriptionPlan":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"price_minor":{"type":"integer"},"currency":{"type":"string"},"billing_period":{"type":"string"},"accent_color":{"type":["string","null"]},"features":{},"sort_order":{"type":"integer"}},"required":["id","code","name","description","price_minor","currency","billing_period","accent_color","sort_order"]},"MySubscription":{"type":"object","properties":{"id":{"type":["string","null"],"format":"uuid"},"plan_id":{"type":["string","null"],"format":"uuid"},"plan":{"$ref":"#/components/schemas/SubscriptionPlan"},"status":{"type":["string","null"]},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]}},"required":["id","plan_id","plan","status","started_at","ended_at"]},"VerifyIapResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","format":"uuid"},"status":{"type":"string","description":"active / expired / revoked / grace / on_hold / paused / pending / unknown"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"auto_renew_status":{"type":["boolean","null"],"description":"ios: autoRenewStatus (1=true)。android: SUBSCRIPTION_STATE_ACTIVE / CANCELED で導出。不明または取得不可の場合は null。"},"expires_at":{"type":["string","null"],"description":"サブスク期限の ISO 8601 文字列。ended_at と同値。"},"environment":{"type":["string","null"],"enum":["Sandbox","Production"],"description":"ios: Apple のレスポンスから取得。android: 常に Production（Play は環境を分離しない）。"},"platform":{"type":"string","enum":["ios","android"]},"external_subscription_id":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","user_id","plan_id","status","started_at","ended_at","auto_renew_status","expires_at","environment","platform","external_subscription_id","created_at","updated_at"]},"VerifyIapRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android"],"description":"購入プラットフォーム。ios = App Store Server API (ES256 JWT → /inApps/v1/transactions/{id})、android = Google Play Developer API (subscriptionsV2 + OAuth2 SA JWT)"},"receipt":{"type":"string","minLength":1,"description":"ios: StoreKit2 が返す JWS (signedTransactionInfo)、またはレガシー base64 レシート。android: BillingClient が返す purchaseToken。"},"product_id":{"type":"string","minLength":1,"description":"subscription_plans.code に対応する productId / subscriptionId。"},"transaction_id":{"type":"string","description":"ios のみ。StoreKit2 がデコード済みの transactionId を渡してくれる場合に指定。未指定時は receipt の JWS ペイロードから transactionId クレームを抽出。"}},"required":["platform","receipt","product_id"]},"RefreshIapRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android"],"description":"verify-iap に渡したものと同じプラットフォーム。"},"receipt":{"type":"string","minLength":1,"description":"ios: 最新の signedTransactionInfo JWS（StoreKit2 が自動更新後に返す）。android: 変わらず purchaseToken（同一トークンで最新状態を取得できる）。"},"product_id":{"type":"string","minLength":1,"description":"subscription_plans.code に対応する productId / subscriptionId。"},"transaction_id":{"type":"string","description":"ios のみ。最新の transactionId があれば指定（省略時は receipt から抽出）。"}},"required":["platform","receipt","product_id"]},"MeErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"severity":{"type":["string","null"]},"status":{"type":"string"},"description":{"type":"string"},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"created_at":{"type":"string"}},"required":["id","parking_lot_id","parking_lot_name","report_type","severity","status","description","evidence_urls","created_at"]},"ParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"created_at":{"type":"string"},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"max_tire_width_mm":{"type":["integer","null"]},"max_parking_duration_min":{"type":["integer","null"]},"entry_method":{"type":["string","null"]},"source":{"type":["string","null"]},"shape_type":{"type":["string","null"]},"area":{},"operator_code":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","created_at"]},"NearbyParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"distance_m":{"type":"number"}},"required":["id","name","address","lat","lng","distance_m"]},"NearbyParkingLotWithRanking":{"allOf":[{"$ref":"#/components/schemas/NearbyParkingLot"},{"type":"object","properties":{"ranking_score":{"type":"number","description":"ランキングスコア 0-100（料金40%+距離40%+星評価20%）"},"ranking_rank":{"type":"integer","description":"score 降順の順位（1-based）"},"is_top3":{"type":"boolean","description":"上位3位バッジ用フラグ"}}}]},"ParkingLotWithDetail":{"allOf":[{"$ref":"#/components/schemas/ParkingLot"},{"type":"object","properties":{"pricing_rules":{"type":"array","items":{}},"images":{"type":"array","items":{}},"tags":{"type":"array","items":{}},"operator":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"color":{"type":"string"}},"required":["id","name","slug","color"]},"is_open_now":{"type":["boolean","null"],"description":"現在営業中かどうか（null = 営業時間未設定）"}}}]},"PricingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"category":{"type":"string"},"rule_order":{"type":"integer"},"time_start":{"type":["string","null"]},"time_end":{"type":["string","null"]},"day_type":{"type":["string","null"]},"per_minutes":{"type":["integer","null"]},"price_minor":{"type":["integer","null"]},"cap_type":{"type":["string","null"]},"cap_duration_hours":{"type":["number","null"]},"cap_price_minor":{"type":["integer","null"]},"cap_repeat":{"type":["boolean","null"]}},"required":["id","parking_lot_id","category","rule_order","time_start","time_end","day_type","per_minutes","price_minor","cap_type","cap_duration_hours","cap_price_minor","cap_repeat"]},"NearbyStationItem":{"type":"object","properties":{"distance_m":{"type":["number","null"]},"walk_min":{"type":["number","null"]},"station":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"code":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]}},"required":["id","name","code","lat","lng"]}},"required":["distance_m","station"]},"ParkingLotImage":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"asset_id":{"type":"string","format":"uuid"},"is_main":{"type":"boolean"},"sort_order":{"type":["integer","null"]}},"required":["id","parking_lot_id","asset_id","is_main","sort_order"]},"FeeCalcResponse":{"type":"object","properties":{"total_amount_minor":{"type":"integer"},"breakdown":{"type":"array","items":{}},"note":{"type":"string"}},"required":["total_amount_minor","breakdown"]},"FeeCalcRequest":{"type":"object","properties":{"entry_at":{"type":"string","format":"date-time"},"exit_at":{"type":"string","format":"date-time"},"vehicle_type":{"type":"string","default":"sedan"}},"required":["entry_at","exit_at"]},"Review":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string"},"owner_reply":{"type":["string","null"],"description":"オーナーからの返信テキスト（未返信時は null）"},"owner_replied_at":{"type":["string","null"],"description":"オーナー返信日時（ISO8601）"}},"required":["id","parking_lot_id","user_id","rating","comment","status","created_at","owner_reply","owner_replied_at"]},"Tag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":["string","null"]},"sort_order":{"type":["integer","null"]},"slug":{"type":["string","null"]},"category":{"type":["string","null"]},"icon_name":{"type":["string","null"]}},"required":["id","name","color","sort_order"]},"ArticleListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"title":{"type":"string"},"excerpt":{"type":["string","null"]},"category":{"type":"string"},"content_format":{"type":["string","null"]},"author_name":{"type":["string","null"]},"author_slug":{"type":["string","null"]},"published_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"hero_image_url":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"related_hubs":{"type":"array","items":{}},"story_number":{"type":["integer","null"]},"view_count":{"type":["integer","null"]}},"required":["id","slug","title","excerpt","category","content_format","author_name","author_slug","published_at","updated_at","thumbnail_url","hero_image_url","tags","related_hubs","story_number","view_count"]},"Article":{"allOf":[{"$ref":"#/components/schemas/ArticleListItem"},{"type":"object","properties":{"updated_at":{"type":"string"},"view_count":{"type":"integer"},"body":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["body","created_at"]}]},"Ad":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"ad_type":{"type":"string"},"placement":{"type":"string"},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"alt_text":{"type":["string","null"]},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]}},"required":["id","name","ad_type","placement","banner_url","link_url","alt_text","start_date","end_date"]},"SupportTicket":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"type":"string"},"priority":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","subject","body","category","priority","status","created_at"]},"ErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"description":{"type":"string"},"severity":{"type":["string","null"]},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"status":{"type":"string"},"photo_asset_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","parking_lot_id","parking_lot_name","report_type","description","severity","evidence_urls","status","photo_asset_id","created_at"]},"ReviewFlagResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"review_id":{"type":"string","format":"uuid"},"reason":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","review_id","reason","status","created_at"]},"AreaSponsor":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"radius_m":{"type":"integer"}},"required":["id","name","category","description","logo_url","banner_url","link_url","phone","address","lat","lng","radius_m"]},"UploadUrlResponse":{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid"},"upload_url":{"type":"string","format":"uri"},"s3_key":{"type":"string"},"public_url":{"type":"string","format":"uri"},"expires_in":{"type":"integer"}},"required":["asset_id","upload_url","s3_key","public_url","expires_in"]},"Asset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"file_name":{"type":"string"},"file_size":{"type":"integer"},"mime_type":{"type":"string"},"s3_key":{"type":"string"},"category":{"type":"string"},"entity_type":{"type":["string","null"]},"entity_id":{"type":["string","null"],"format":"uuid"},"is_public":{"type":"boolean"},"uploaded_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","file_name","file_size","mime_type","s3_key","category","entity_type","entity_id","is_public","uploaded_by","created_at"]},"SearchLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"max_tire_width_mm":{"type":["integer","null"]},"max_parking_duration_min":{"type":["integer","null"]},"entry_method":{"type":["string","null"]},"shape_type":{"type":["string","null"]},"area":{},"operator_code":{"type":["string","null"]},"operator_name":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]},"pricing_rules":{"type":"array","items":{}},"tags":{"type":"array","items":{}}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","max_height_m","max_width_m","max_length_m","max_weight_t","min_clearance_cm","max_tire_width_mm","max_parking_duration_min","entry_method","shape_type","operator_code","operator_name","entry_difficulty","pricing_rules","tags"]},"AiSearchQuery":{"type":"object","properties":{"keywords":{"type":"array","items":{"type":"string"}},"max_price_per_hour":{"type":"integer"},"roof":{"type":"boolean"},"open_24h":{"type":"boolean"},"vehicle_type":{"type":"string","enum":["sedan","kei","minivan","suv","truck"]}}},"AiSearchResponse":{"type":"object","properties":{"status":{"type":"string","enum":["parsed","need_info","error"]},"query":{"$ref":"#/components/schemas/AiSearchQuery"},"reply":{"type":"string"}},"required":["status","reply"]},"AiSearchRequest":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":500},"session_id":{"type":"string"}},"required":["message"]},"HubStation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"lines":{"type":"array","items":{"type":"string"}},"code":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"city":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"slug":{"type":["string","null"]}},"required":["id","name","slug"]},"prefecture":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"name_en":{"type":["string","null"]},"slug":{"type":["string","null"]}},"required":["id","name","slug"]}},"required":["id","name","code","lat","lng","city","prefecture"]},"HubPublishableItem":{"type":"object","properties":{"stats":{"type":"object","properties":{"station_id":{"type":"string","format":"uuid"},"total_count":{"type":"integer"},"in_stock_count":{"type":"integer"},"out_of_stock_count":{"type":"integer"}},"required":["station_id","total_count","in_stock_count","out_of_stock_count"]},"station":{"$ref":"#/components/schemas/HubStation"}},"required":["stats","station"]},"HubParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"total_spaces":{"type":["integer","null"]},"status":{"type":["string","null"]},"structure":{"type":["string","null"]},"operating_hours":{"type":["string","null"]},"max_height_m":{"type":["number","null"]},"max_width_m":{"type":["number","null"]},"max_length_m":{"type":["number","null"]},"max_weight_t":{"type":["number","null"]},"min_clearance_cm":{"type":["integer","null"]},"operator_code":{"type":["string","null"]},"operator_name":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]},"pricing_rules":{"type":"array","items":{}},"tags":{"type":"array","items":{}}},"required":["id","name","address","lat","lng","total_spaces","status","structure","operating_hours","max_height_m","max_width_m","max_length_m","max_weight_t","min_clearance_cm","operator_code","operator_name","entry_difficulty","pricing_rules","tags"]},"HubParkingLotItem":{"type":"object","properties":{"distance_m":{"type":["number","null"]},"walk_min":{"type":["number","null"]},"parking_lot":{"$ref":"#/components/schemas/HubParkingLot"}},"required":["distance_m","parking_lot"]},"ActivityTypeMeta":{"type":"object","properties":{"type":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["session","engagement","asset","discovery","account"]},"emitted_by":{"type":"string"},"emitted":{"type":"boolean"},"metadata_schema":{}},"required":["type","description","category","emitted_by","emitted"]},"ActivityTypesResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ActivityTypeMeta"}}},"required":["items"]},"DeepLinkItem":{"type":"object","properties":{"pattern":{"type":"string","examples":["/parking-lots/:id"]},"screen":{"type":"string","examples":["ParkingLotDetail"]},"params":{"type":"array","items":{"type":"string"},"examples":[["id"]]},"requires_auth":{"type":"boolean","description":"未設定時は true 扱い（認証必須）。false のものは未ログインでも遷移できる。"}},"required":["pattern","screen","params"]},"DeepLinksResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DeepLinkItem"}}},"required":["items"]},"AppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","examples":["1.0.0"]},"min_app_version_android":{"type":"string","examples":["1.0.0"]},"is_maintenance":{"type":"boolean","examples":[false]},"maintenance_message":{"type":["string","null"]},"store_url_ios":{"type":["string","null"]},"store_url_android":{"type":["string","null"]},"updated_at":{"type":"string"}},"required":["min_app_version_ios","min_app_version_android","is_maintenance","maintenance_message","store_url_ios","store_url_android","updated_at"]},"DataExport":{"type":"object","properties":{"export_date":{"type":"string"},"user_id":{"type":"string","format":"uuid"},"profile":{},"parking_sessions":{"type":"array","items":{}},"reviews":{"type":"array","items":{}},"ratings":{"type":"array","items":{}},"saved_parking_lots":{"type":"array","items":{}},"vehicles":{"type":"array","items":{}},"search_presets":{"type":"array","items":{}},"registered_device_count":{"type":"number"}},"required":["export_date","user_id","parking_sessions","reviews","ratings","saved_parking_lots","vehicles","search_presets","registered_device_count"]},"MyReferralCode":{"type":"object","properties":{"code":{"type":"string"},"usage_count":{"type":"integer"},"max_usages":{"type":["integer","null"]}},"required":["code","usage_count","max_usages"]},"ApplyReferralResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"referrer_user_id":{"type":"string","format":"uuid"},"error":{"type":"string"}},"required":["ok"]},"ReferralHistoryItem":{"type":"object","properties":{"applied_at":{"type":"string"},"code":{"type":"string"},"referee_label":{"type":"string"}},"required":["applied_at","code","referee_label"]},"ReferralHistory":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ReferralHistoryItem"}}},"required":["items"]},"ConsentType":{"type":"object","properties":{"code":{"type":"string","examples":["terms_of_service"]},"current_version":{"type":"string","examples":["2026-04-01"]},"required":{"type":"boolean","examples":[true]},"display_label":{"type":"string","examples":["利用規約"]},"document_url":{"type":["string","null"],"examples":["https://parky.co.jp/terms"]},"updated_at":{"type":"string","examples":["2026-04-01T00:00:00Z"]}},"required":["code","current_version","required","display_label","document_url","updated_at"]},"ConsentItem":{"type":"object","properties":{"consent_type":{"type":"string","examples":["terms_of_service"]},"version":{"type":"string","examples":["2026-04-01"]},"granted":{"type":"boolean","examples":[true]},"granted_at":{"type":"string","examples":["2026-04-21T03:00:00Z"]},"source":{"type":["string","null"],"examples":["mobile_ios"]}},"required":["consent_type","version","granted","granted_at","source"]},"ConsentPostBody":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"consent_type":{"type":"string","minLength":1,"examples":["terms_of_service"]},"version":{"type":"string","minLength":1,"examples":["2026-04-01"]},"granted":{"type":"boolean"},"source":{"type":"string","examples":["mobile_ios"]}},"required":["consent_type","version","granted"]},"minItems":1,"maxItems":20}},"required":["items"]},"ClientEventCreated":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"created_at":{"type":"string"}},"required":["id","created_at"]},"SharePublic":{"type":"object","properties":{"parking_session_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"parking_lot_name":{"type":["string","null"]},"start_lat":{"type":["number","null"]},"start_lng":{"type":["number","null"]},"start_at":{"type":["string","null"]},"status":{"type":"string"},"access_count":{"type":"integer"},"expires_at":{"type":"string"}},"required":["parking_session_id","parking_lot_id","parking_lot_name","start_lat","start_lng","start_at","status","access_count","expires_at"]},"Admin":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"last_login_at":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","name","email","role_id","status","last_login_at","created_at"]},"AdminNotificationPrefs":{"type":"object","properties":{"new_owner":{"type":"boolean","default":true},"new_parking":{"type":"boolean","default":true},"sales_daily":{"type":"boolean","default":true},"sales_monthly":{"type":"boolean","default":true},"system_alert":{"type":"boolean","default":true}}},"Role":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"color":{"type":"string"},"is_system":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","name","description","color","is_system","created_at"]},"AdminParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"status":{"type":["string","null"]},"total_spaces":{"type":["integer","null"]},"operating_hours":{"type":["string","null"]},"operator_code":{"type":["string","null"]},"entry_difficulty":{"type":["string","null"]}},"required":["id","name","address","lat","lng","status","total_spaces","operating_hours"]},"AdminAppUser":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"auth_user_id":{"type":["string","null"],"format":"uuid"},"display_name":{"type":"string"},"email":{"type":"string"},"vehicle_type":{"type":["string","null"]},"premium":{"type":"boolean"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","auth_user_id","display_name","email","vehicle_type","premium","status","created_at"]},"AdminTag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"color":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"slug":{"type":["string","null"]},"category":{"type":["string","null"]},"icon_name":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","color","sort_order","created_at","slug","category","icon_name"]},"AdminOperator":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"color":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"deleted_at":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","slug","color","sort_order","created_at","updated_at","deleted_at"]},"AdminNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"message":{"type":"string"},"category":{"type":"string"},"avatar":{"type":"string"},"read":{"type":"boolean"},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","title","message","category","avatar","read","created_at","deleted_at"]},"AdminUserNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"type":{"type":"string"},"target":{"type":"string"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"total_recipients":{"type":["integer","null"]},"success_count":{"type":["integer","null"]},"failure_count":{"type":["integer","null"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","title","body","type","target","status","scheduled_at","sent_at","total_recipients","success_count","failure_count","created_at","deleted_at"]},"DispatchEnqueued":{"type":"object","properties":{"notification_id":{"type":"string","format":"uuid"},"enqueued":{"type":"integer"},"batches":{"type":"integer"}},"required":["notification_id","enqueued","batches"]},"AdminReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"rating":{"type":"integer"},"comment":{"type":["string","null"]},"status":{"type":"string"},"admin_note":{"type":["string","null"]},"created_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","parking_lot_id","user_id","rating","comment","status","admin_note","created_at","deleted_at"]},"AdminSupportTicket":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"category":{"type":"string"},"priority":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["id","user_id","user_email","user_name","subject","body","category","priority","status","created_at"]},"AdminErrorReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"user_email":{"type":"string"},"user_name":{"type":"string"},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"parking_lot_name":{"type":"string"},"report_type":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"severity":{"type":["string","null"]},"evidence_urls":{"type":["array","null"],"items":{"type":"string"}},"resolved_by":{"type":["string","null"],"format":"uuid"},"admin_note":{"type":["string","null"]},"created_at":{"type":"string"},"resolved_at":{"type":["string","null"]},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"required":["id","user_id","user_email","user_name","parking_lot_id","parking_lot_name","report_type","description","status","severity","evidence_urls","resolved_by","admin_note","created_at","resolved_at"]},"AdminTask":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"task_kind":{"type":"string"},"ref_id":{"type":"string","format":"uuid"},"assignee_id":{"type":["string","null"],"format":"uuid"},"urgency":{"type":"string"},"due_at":{"type":["string","null"]},"memo":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","task_kind","ref_id","assignee_id","urgency","due_at","memo","created_at","updated_at"]},"AdminArticle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":["string","null"]},"title":{"type":"string"},"body":{"type":["string","null"]},"excerpt":{"type":["string","null"]},"category":{"type":"string"},"author_name":{"type":["string","null"]},"status":{"type":"string"},"published_at":{"type":["string","null"]},"view_count":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"thumbnail_url":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"publish_to_web":{"type":"boolean"},"publish_to_app":{"type":"boolean"},"hero_image_url":{"type":["string","null"]},"author_slug":{"type":["string","null"]},"content_format":{"type":"string"},"related_hubs":{}},"required":["id","slug","title","body","excerpt","category","author_name","status","published_at","view_count","tags","thumbnail_url","created_at","updated_at"]},"AdminArticleTag":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"sort_order":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","name","sort_order","created_at"]},"AdminAd":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"ad_type":{"type":"string"},"placement":{"type":"string"},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"alt_text":{"type":["string","null"]},"status":{"type":"string"},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"memo":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","ad_type","placement","banner_url","link_url","alt_text","status","start_date","end_date","impressions","clicks","memo","created_at","updated_at"]},"AdminBoost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"credit_per_impression":{"type":"integer"},"daily_budget":{"type":["integer","null"]},"total_budget":{"type":["integer","null"]},"status":{"type":"string"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","name","email"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]}},"required":["id","owner_id","parking_lot_id","name","credit_per_impression","daily_budget","total_budget","status","started_at","ended_at"]},"AdminSponsor":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"link_url":{"type":["string","null"]}},"required":["id","name","category","description","logo_url","banner_url","link_url"]},"AdminOwner":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_type":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":["string","null"]},"status":{"type":"string"},"company_name":{"type":["string","null"]},"representative_name":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parking_count":{"type":"integer"}},"required":["id","owner_type","name","email","phone","status","company_name","representative_name","created_at","updated_at"]},"AdminOwnerCredit":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"balance":{"type":"integer"},"total_purchased":{"type":"integer"},"total_consumed":{"type":"integer"},"updated_at":{"type":["string","null"]},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"owner_type":{"type":["string","null"]}},"required":["id","name","email","owner_type"]}},"required":["id","owner_id","balance","total_purchased","total_consumed","owner"]},"AdminOwnerApplication":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":["string","null"],"format":"uuid"},"owner_name":{"type":"string"},"owner_email":{"type":"string"},"company_name":{"type":["string","null"]},"parking_lot_id":{"type":"string","format":"uuid"},"proof_asset_id":{"type":["string","null"],"format":"uuid"},"additional_notes":{"type":["string","null"]},"status":{"type":"string"},"reviewed_by":{"type":["string","null"],"format":"uuid"},"reviewed_at":{"type":["string","null"]},"reject_reason":{"type":["string","null"]},"created_at":{"type":"string"},"owner":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"owner_type":{"type":["string","null"]}},"required":["id","name","email","owner_type"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]},"proof_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"reviewer":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}},"required":["id","name"]}},"required":["id","owner_id","owner_name","owner_email","company_name","parking_lot_id","status","reviewed_by","reviewed_at","reject_reason","created_at"]},"AdminActivityLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"admin_id":{"type":["string","null"],"format":"uuid"},"admin_email_snapshot":{"type":"string"},"action":{"type":"string"},"target_type":{"type":["string","null"]},"target_id":{"type":["string","null"]},"target_label":{"type":["string","null"]},"metadata":{},"ip_address":{"type":["string","null"]},"user_agent":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","admin_id","admin_email_snapshot","action","target_type","target_id","target_label","ip_address","user_agent","created_at"]},"RevenueMonthlyRow":{"type":"object","properties":{"month":{"type":"string"},"channel":{"type":"string"},"amount_minor":{"type":"integer"},"count":{"type":"integer"}},"required":["month","channel","amount_minor","count"]},"RevenueTransaction":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"occurred_at":{"type":"string"},"channel":{"type":"string"},"transaction_type":{"type":"string"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"external_id":{"type":["string","null"]},"description":{"type":["string","null"]},"user_id":{"type":["string","null"],"format":"uuid"},"plan_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","occurred_at","channel","transaction_type","amount_minor","currency","external_id","description","user_id","plan_id","created_at"]},"RevenueTransactionWithRelations":{"allOf":[{"$ref":"#/components/schemas/RevenueTransaction"},{"type":"object","properties":{"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","display_name","email"]},"plan":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":["string","null"]},"name":{"type":["string","null"]}},"required":["id","code","name"]}},"required":["user","plan"]}]},"AdminUserSubscription":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"started_at":{"type":"string"},"ended_at":{"type":["string","null"]},"external_subscription_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","plan_id","status","started_at","ended_at","external_subscription_id","created_at"]},"AdminSubscriptionPlan":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"price_minor":{"type":"integer"},"currency":{"type":"string"},"billing_period":{"type":"string"},"accent_color":{"type":["string","null"]},"features":{},"sort_order":{"type":"integer"},"is_active":{"type":"boolean"}},"required":["id","code","name","description","price_minor","currency","billing_period","accent_color","sort_order","is_active"]},"StoreIntegration":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"display_name":{"type":"string"},"package_or_app_id":{"type":["string","null"]},"vendor_number":{"type":["string","null"]},"connection_status":{"type":"string"},"last_status_message":{"type":["string","null"]},"last_synced_at":{"type":["string","null"]},"last_sync_error":{"type":["string","null"]}},"required":["id","store","display_name","package_or_app_id","vendor_number","connection_status","last_status_message","last_synced_at","last_sync_error"]},"StoreSalesRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"report_date":{"type":"string"},"product_id":{"type":"string"},"country_code":{"type":"string"},"units":{"type":"integer"},"proceeds_minor":{"type":"integer"},"gross_minor":{"type":"integer"},"currency":{"type":"string"},"fetched_at":{"type":"string"}},"required":["id","store","report_date","product_id","country_code","units","proceeds_minor","gross_minor","currency","fetched_at"]},"StoreMetricsRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"metric_date":{"type":"string"},"installs":{"type":["integer","null"]},"uninstalls":{"type":["integer","null"]},"active_devices":{"type":["integer","null"]},"impressions":{"type":["integer","null"]},"product_page_views":{"type":["integer","null"]},"crash_rate":{"type":["number","null"]},"anr_rate":{"type":["number","null"]},"rating_average":{"type":["number","null"]},"rating_count":{"type":["integer","null"]}},"required":["id","store","metric_date","installs","uninstalls","active_devices","impressions","product_page_views","crash_rate","anr_rate","rating_average","rating_count"]},"StoreReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"external_review_id":{"type":"string"},"author_name":{"type":["string","null"]},"rating":{"type":["integer","null"]},"title":{"type":["string","null"]},"body":{"type":["string","null"]},"reply_body":{"type":["string","null"]},"reply_at":{"type":["string","null"]},"posted_at":{"type":"string"}},"required":["id","store","external_review_id","author_name","rating","title","body","reply_body","reply_at","posted_at"]},"StoreSyncRun":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"store":{"type":"string"},"task":{"type":"string"},"status":{"type":"string"},"started_at":{"type":"string"},"finished_at":{"type":["string","null"]},"rows_upserted":{"type":"integer"},"message":{"type":["string","null"]},"triggered_by":{"type":"string"}},"required":["id","store","task","status","started_at","finished_at","rows_upserted","message","triggered_by"]},"BadgeDefinition":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"activity_type":{"type":"string"},"threshold":{"type":"integer"},"sort_order":{"type":"integer"},"is_active":{"type":"boolean"},"asset_id":{"type":["string","null"],"format":"uuid"},"conditions":{},"category":{"type":"string"},"created_at":{"type":"string"},"holders_count":{"type":"integer"},"asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"tags":{"type":"array","items":{"type":"string"}}},"required":["id","name","description","icon","activity_type","threshold","sort_order","is_active","asset_id","category","created_at"]},"ActivityExpRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"activity_type":{"type":"string"},"exp_amount":{"type":"integer"},"description":{"type":"string"},"is_active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","activity_type","exp_amount","description","is_active","created_at","updated_at"]},"LevelDefinition":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"level":{"type":"integer"},"required_exp":{"type":"integer"},"created_at":{"type":"string"}},"required":["id","level","required_exp","created_at"]},"AdminTheme":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"is_free":{"type":"boolean"},"price_yen_minor":{"type":["integer","null"]},"preview_asset_id":{"type":["string","null"],"format":"uuid"},"is_active":{"type":"boolean"},"sort_order":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"preview_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"owners_count":{"type":"integer"},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"theme_id":{"type":"string","format":"uuid"},"category":{"type":"string"},"part_id":{"type":"string","format":"uuid"}},"required":["id","theme_id","category","part_id"]}}},"required":["id","name","description","is_free","price_yen_minor","preview_asset_id","is_active","sort_order","created_at","updated_at"]},"AdminThemePart":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"category":{"type":"string"},"primary_color":{"type":["string","null"]},"accent_color":{"type":["string","null"]},"pin_asset_id":{"type":["string","null"],"format":"uuid"},"icon_asset_id":{"type":["string","null"],"format":"uuid"},"loading_asset_id":{"type":["string","null"],"format":"uuid"},"loading_type":{"type":["string","null"]},"is_active":{"type":"boolean"},"sort_order":{"type":"integer"},"used_in_themes_count":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"pin_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"icon_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]},"loading_asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]}},"required":["id","name","description","category","primary_color","accent_color","pin_asset_id","icon_asset_id","loading_asset_id","loading_type","is_active","sort_order"]},"AiProvider":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider_key":{"type":"string"},"display_name":{"type":"string"},"model_name":{"type":"string"},"vault_secret_id":{"type":["string","null"],"format":"uuid"},"is_enabled":{"type":"boolean"},"priority":{"type":"integer"},"config":{},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","provider_key","display_name","model_name","vault_secret_id","is_enabled","priority","created_at","updated_at"]},"AiUsageLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider_key":{"type":"string"},"user_input":{"type":"string"},"parsed_query":{},"response_status":{"type":"string"},"attempt_number":{"type":"integer"},"fallback_reason":{"type":["string","null"]},"error_code":{"type":["string","null"]},"error_message":{"type":["string","null"]},"session_id":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","provider_key","user_input","response_status","attempt_number","fallback_reason","error_code","error_message","session_id","created_at"]},"AdminCode":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category_id":{"type":"string"},"code":{"type":"string"},"display_label":{"type":"string"},"lang":{"type":"string"},"sort_order":{"type":["integer","null"]},"is_deleted":{"type":"boolean"}},"required":["id","category_id","code","display_label","lang","sort_order","is_deleted"]},"AdminParkingSession":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"vehicle_id":{"type":["string","null"],"format":"uuid"},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]},"status":{"type":"string"},"total_amount_minor":{"type":["number","null"]},"memo":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","parking_lot_id","vehicle_id","started_at","ended_at","status","total_amount_minor","created_at"]},"AdminParkingSessionWithRelations":{"allOf":[{"$ref":"#/components/schemas/AdminParkingSession"},{"type":"object","properties":{"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","display_name","email"]},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]}},"required":["id","name","address"]}},"required":["user","parking_lot"]}]},"IgSlideCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"prefix":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"integer"}},"required":["code","label","prefix","sort_order","is_deleted"]},"IgPostCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"color":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"integer"}},"required":["code","label","color","sort_order","is_deleted"]},"IgTag":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":["string","null"]},"usage_count":{"type":"integer"}},"required":["id","name","color","usage_count"]},"IgPostTemplate":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"slide_refs":{"type":"string"}},"required":["id","code","name","description","slide_refs"]},"IgTemplate":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"slide_type":{"type":"string"},"html_body":{"type":"string"},"slot_schema":{"type":"string"},"sample_content":{"type":"string","default":"{}"},"sample_html":{"type":"string","default":""},"uses_parking_lot":{"type":"integer","default":0},"sort_order":{"type":"integer"},"is_active":{"type":"integer"}},"required":["id","code","name","slide_type","html_body","slot_schema","sort_order","is_active"]},"IgCampaign":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"title":{"type":"string"},"theme":{"type":["string","null"]},"area":{"type":["string","null"]},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"ig_media_id":{"type":["string","null"]},"notes":{"type":["string","null"]},"source_material":{"type":["string","null"]},"post_category_code":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","code","title","theme","area","status","scheduled_at","ig_media_id","notes","source_material","post_category_code","created_by","created_at","updated_at"]},"IgSlide":{"type":"object","properties":{"id":{"type":"string"},"campaign_id":{"type":"string"},"template_id":{"type":"string"},"slide_index":{"type":"integer"},"content":{"type":"string"},"html_override":{"type":["string","null"]},"png_r2_key":{"type":["string","null"]},"png_url":{"type":["string","null"]},"revision_notes":{"type":["string","null"]},"parking_lot_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","campaign_id","template_id","slide_index","content","html_override","png_r2_key","png_url","revision_notes","created_at","updated_at"]},"IgCaption":{"type":["object","null"],"properties":{"id":{"type":"string"},"campaign_id":{"type":"string"},"body":{"type":["string","null"]},"hashtags":{"type":["string","null"]},"draft_body":{"type":["string","null"]},"generated_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","campaign_id","body","hashtags","draft_body","generated_at","created_at","updated_at"]},"IgCompetitorSnapshot":{"type":"object","properties":{"id":{"type":"string"},"campaign_id":{"type":["string","null"]},"source_url":{"type":["string","null"]},"account_handle":{"type":["string","null"]},"raw_notes":{"type":["string","null"]},"ai_ideas":{"type":["string","null"]},"created_by":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","campaign_id","source_url","account_handle","raw_notes","ai_ideas","created_by","created_at"]},"AdminAsset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category":{"type":"string"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"file_size":{"type":"integer"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"created_at":{"type":"string"},"updated_at":{"type":["string","null"]}},"required":["id","category","s3_key","file_name","mime_type","file_size","width","height","created_at"]},"AdminUserVehicle":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"nickname":{"type":["string","null"]},"vehicle_type_code":{"type":["string","null"]},"maker_code":{"type":["string","null"]},"size_code":{"type":["string","null"]},"color_code":{"type":["string","null"]},"model_name":{"type":["string","null"]},"year":{"type":["integer","null"]},"asset_id":{"type":["string","null"],"format":"uuid"},"is_primary":{"type":"boolean"},"notes":{"type":["string","null"]},"deleted_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]},"asset":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"s3_key":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"category":{"type":["string","null"]}},"required":["id","s3_key","file_name","mime_type","category"]}},"required":["id","user_id","nickname","vehicle_type_code","maker_code","size_code","color_code","model_name","year","asset_id","is_primary","notes","deleted_at","created_at","updated_at"]},"AdminUserSearchPreset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"query_json":{"$ref":"#/components/schemas/SearchQueryV1"},"is_default":{"type":"boolean"},"sort_order":{"type":"integer"},"deleted_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"user":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]}},"required":["id","display_name"]}},"required":["id","user_id","name","query_json","is_default","sort_order","deleted_at","created_at","updated_at"]},"PlacesImportLotResult":{"type":"object","properties":{"lot_id":{"type":"string","format":"uuid"},"fetched":{"type":"integer"},"upserted":{"type":"integer"},"skipped":{"type":"integer"},"auto_on_map":{"type":"integer"},"auto_on_facility":{"type":"integer"}},"required":["lot_id","fetched","upserted","skipped","auto_on_map","auto_on_facility"]},"PlacesImportAllResult":{"type":"object","properties":{"total_lots":{"type":"integer"},"processed":{"type":"integer"},"total_upserted":{"type":"integer"},"errors":{"type":"array","items":{"type":"object","properties":{"lot_id":{"type":"string","format":"uuid"},"message":{"type":"string"}},"required":["lot_id","message"]}},"dry_run":{"type":"boolean"}},"required":["total_lots","processed","total_upserted","errors","dry_run"]},"AdminAreaPlace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"category":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"link_url":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"radius_m":{"type":"integer"},"status":{"type":"string"},"is_sponsored":{"type":"boolean"},"source":{"type":"string"},"source_place_id":{"type":["string","null"]},"rating":{"type":["number","null"]},"user_ratings_total":{"type":["integer","null"]},"place_types":{"type":["array","null"],"items":{"type":"string"}},"show_on_map":{"type":"boolean"},"show_as_facility":{"type":"boolean"},"last_refreshed_at":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]}},"required":["id","name","category","lat","lng","radius_m","status","is_sponsored","source","show_on_map","show_as_facility","created_at","updated_at"]},"AdminPlaceDiscount":{"type":"object","properties":{"place_id":{"type":"string","format":"uuid"},"lot_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":["string","null"]},"terms":{"type":["string","null"]},"valid_from":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"deleted_at":{"type":["string","null"]},"place_name":{"type":"string"},"lot_name":{"type":"string"}},"required":["place_id","lot_id","title","created_at","updated_at"]},"AdminAppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","examples":["1.0.0"]},"min_app_version_android":{"type":"string","examples":["1.0.0"]},"is_maintenance":{"type":"boolean","examples":[false]},"maintenance_message":{"type":["string","null"]},"store_url_ios":{"type":["string","null"]},"store_url_android":{"type":["string","null"]},"updated_at":{"type":"string"},"updated_by_admin_id":{"type":["string","null"],"format":"uuid"}},"required":["min_app_version_ios","min_app_version_android","is_maintenance","maintenance_message","store_url_ios","store_url_android","updated_at","updated_by_admin_id"]},"UpdateAppConfig":{"type":"object","properties":{"min_app_version_ios":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","examples":["1.2.0"]},"min_app_version_android":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","examples":["1.2.0"]},"is_maintenance":{"type":"boolean"},"maintenance_message":{"type":["string","null"],"maxLength":500},"store_url_ios":{"type":["string","null"],"format":"uri"},"store_url_android":{"type":["string","null"],"format":"uri"}}},"AdminClientEventListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"event_type":{"type":"string"},"severity":{"type":"string"},"message":{"type":["string","null"]},"app_version":{"type":["string","null"]},"platform":{"type":["string","null"]},"os_version":{"type":["string","null"]},"device_model":{"type":["string","null"]},"parking_session_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","event_type","severity","message","app_version","platform","os_version","device_model","parking_session_id","created_at"]},"AdminClientEventDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"event_type":{"type":"string"},"severity":{"type":"string"},"message":{"type":["string","null"]},"stack_trace":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":{}},"app_version":{"type":["string","null"]},"platform":{"type":["string","null"]},"os_version":{"type":["string","null"]},"device_model":{"type":["string","null"]},"parking_session_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","user_id","event_type","severity","message","stack_trace","metadata","app_version","platform","os_version","device_model","parking_session_id","created_at"]},"SnsMetricsSeriesItem":{"type":"object","properties":{"week":{"type":"string","examples":["2026-W15"]},"followers":{"type":"integer"},"engagementRate":{"type":"number"}},"required":["week","followers","engagementRate"]},"SnsMetricsPlatform":{"type":"object","properties":{"platform":{"type":"string","enum":["instagram","x"]},"followers":{"type":"integer"},"engagementRate":{"type":"number"},"weeklyDiffFollowers":{"type":"integer"},"weeklyDiffEngagementPt":{"type":"number"},"monthlyDiffFollowers":{"type":"integer"},"series":{"type":"array","items":{"$ref":"#/components/schemas/SnsMetricsSeriesItem"}}},"required":["platform","followers","engagementRate","weeklyDiffFollowers","weeklyDiffEngagementPt","monthlyDiffFollowers","series"]},"SnsMetricsResponse":{"type":"object","properties":{"platforms":{"type":"array","items":{"$ref":"#/components/schemas/SnsMetricsPlatform"}}},"required":["platforms"]},"PostCalendarDay":{"type":"object","properties":{"date":{"type":"string","examples":["2026-04-01"]},"instagram":{"type":"object","properties":{"draft":{"type":"integer"},"scheduled":{"type":"integer"},"published":{"type":"integer"}},"required":["draft","scheduled","published"]},"x":{"type":"object","properties":{"draft":{"type":"integer"},"scheduled":{"type":"integer"},"published":{"type":"integer"}},"required":["draft","scheduled","published"]}},"required":["date","instagram","x"]},"PostCalendarResponse":{"type":"object","properties":{"days":{"type":"array","items":{"$ref":"#/components/schemas/PostCalendarDay"}}},"required":["days"]},"ArticlesPvItem":{"type":"object","properties":{"id":{"anyOf":[{"type":"integer"},{"type":"string"}]},"slug":{"type":["string","null"]},"title":{"type":"string"},"pv":{"type":"integer"},"pvDiffPct":{"type":"number"}},"required":["id","slug","title","pv","pvDiffPct"]},"ArticlesPvResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ArticlesPvItem"}},"sources":{"type":"object","properties":{"direct":{"type":"integer"},"google":{"type":"integer"},"twitter":{"type":"integer"},"facebook":{"type":"integer"},"other":{"type":"integer"}},"required":["direct","google","twitter","facebook","other"]}},"required":["items","sources"]},"AdsCtrDaily":{"type":"object","properties":{"date":{"type":"string"},"clicks":{"type":"integer"}},"required":["date","clicks"]},"AdsCtrItem":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"ctr":{"type":"number"},"daysRemaining":{"type":"integer"},"daily":{"type":"array","items":{"$ref":"#/components/schemas/AdsCtrDaily"}}},"required":["id","name","startDate","endDate","impressions","clicks","ctr","daysRemaining","daily"]},"AdsCtrResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdsCtrItem"}}},"required":["items"]},"MarketingSubscriber":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string"},"source":{"type":["string","null"]},"lang":{"type":["string","null"]},"subscribed_at":{"type":"string"},"unsubscribed_at":{"type":["string","null"]},"meta":{},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","email","source","lang","subscribed_at","unsubscribed_at","created_at","updated_at"]},"MarketingBroadcast":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"subject":{"type":["string","null"]},"body_markdown":{"type":["string","null"]},"segment":{"type":["string","null"]},"status":{"type":"string"},"engine":{"type":["string","null"]},"scheduled_at":{"type":["string","null"]},"sent_at":{"type":["string","null"]},"delivered":{"type":"integer"},"opened":{"type":"integer"},"clicked":{"type":"integer"},"open_rate":{"type":"number"},"click_rate":{"type":"number"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","title","subject","body_markdown","segment","status","engine","scheduled_at","sent_at","delivered","opened","clicked","open_rate","click_rate","created_at","updated_at"]},"MarketingXAccount":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"handle":{"type":"string"},"display_name":{"type":["string","null"]},"tier":{"type":["string","null"]},"is_primary":{"type":"boolean"},"followers_count":{"type":"integer"},"has_token":{"type":"boolean"},"updated_at":{"type":["string","null"]},"following":{"type":"integer"},"postsTotal":{"type":"integer"},"verified":{"type":"boolean"},"bio":{"type":["string","null"]},"displayName":{"type":["string","null"]},"followers":{"type":"integer"},"updatedAt":{"type":["string","null"]}},"required":["id","handle","display_name","tier","is_primary","followers_count","has_token","updated_at","following","postsTotal","verified","bio","displayName","followers","updatedAt"]},"MarketingXPost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":["string","null"],"format":"uuid"},"body":{"type":"string"},"thread_of":{"type":["string","null"],"format":"uuid"},"threadCount":{"type":"integer"},"status":{"type":"string"},"scheduled_at":{"type":["string","null"]},"published_at":{"type":["string","null"]},"x_tweet_id":{"type":["string","null"]},"impressions":{"type":"integer"},"likes":{"type":"integer"},"reposts":{"type":"integer"},"replies":{"type":"integer"},"error_message":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"accountId":{"type":["string","null"],"format":"uuid"},"threadOf":{"type":["string","null"],"format":"uuid"},"scheduledAt":{"type":["string","null"]},"publishedAt":{"type":["string","null"]},"xTweetId":{"type":["string","null"]},"errorMessage":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","account_id","body","thread_of","threadCount","status","scheduled_at","published_at","x_tweet_id","impressions","likes","reposts","replies","error_message","created_at","updated_at","accountId","threadOf","scheduledAt","publishedAt","xTweetId","errorMessage","createdAt","updatedAt"]},"MarketingXScheduleRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"dow":{"type":"array","items":{"type":"integer"}},"hour":{"type":"integer"},"minute":{"type":"integer"},"source_kind":{"type":"string"},"source_ref":{"type":["string","null"]},"category":{"type":["string","null"]},"enabled":{"type":"boolean"},"last_fired_at":{"type":["string","null"]},"created_at":{"type":"string"},"weekdays":{"type":"array","items":{"type":"integer"}},"sourceType":{"type":"string"},"sourceLabel":{"type":["string","null"]},"categoryFilter":{"type":"string"},"lastFiredAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","name","dow","hour","minute","source_kind","source_ref","category","enabled","last_fired_at","created_at","weekdays","sourceType","sourceLabel","categoryFilter","lastFiredAt","createdAt"]},"MarketingXListenRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"listen_kind":{"type":"string"},"target":{"type":"string"},"min_likes_threshold":{"type":["integer","null"]},"must_contain":{"type":["string","null"]},"must_not_contain":{"type":["string","null"]},"action_kinds":{"type":"array","items":{"type":"string"}},"reply_template":{"type":["string","null"]},"cooldown_minutes":{"type":"integer"},"enabled":{"type":"boolean"},"last_fired_at":{"type":["string","null"]},"created_at":{"type":"string"},"type":{"type":"string"},"minLikes":{"type":"integer"},"keywordContains":{"type":"string"},"actions":{"type":"array","items":{"type":"string"}},"replyTemplate":{"type":["string","null"]},"cooldownMinutes":{"type":"integer"},"lastFiredAt":{"type":["string","null"]},"createdAt":{"type":"string"}},"required":["id","name","listen_kind","target","min_likes_threshold","must_contain","must_not_contain","action_kinds","reply_template","cooldown_minutes","enabled","last_fired_at","created_at","type","minLikes","keywordContains","actions","replyTemplate","cooldownMinutes","lastFiredAt","createdAt"]},"MarketingXAutomationLog":{"type":"object","properties":{"id":{"type":"string"},"firedAt":{"type":"string"},"ruleName":{"type":"string"},"sourcePost":{"type":"string"},"author":{"type":"string"},"actionTaken":{"type":"string"},"rule_id":{"type":["string","null"],"format":"uuid"},"rule_kind":{"type":["string","null"]},"fired_at":{"type":"string"},"target_tweet_id":{"type":["string","null"]},"action":{"type":["string","null"]},"status":{"type":["string","null"]},"error":{"type":["string","null"]}},"required":["id","firedAt","ruleName","sourcePost","author","actionTaken","rule_id","rule_kind","fired_at","target_tweet_id","action","status","error"]},"MarketingXCompetitor":{"type":"object","properties":{"id":{"type":"string"},"handle":{"type":"string"},"display_name":{"type":["string","null"]},"memo":{"type":["string","null"]},"followers_count":{"type":"integer"},"posts_per_day":{"type":["number","null"]},"added_at":{"type":"string"},"displayName":{"type":"string"},"followers":{"type":"integer"},"postsPerWeek":{"type":"number"},"engagementRate":{"type":"number"},"topPost":{"type":"object","properties":{"body":{"type":"string"},"likes":{"type":"integer"},"postedAt":{"type":"string"}},"required":["body","likes","postedAt"]},"addedAt":{"type":"string"}},"required":["id","handle","display_name","memo","followers_count","posts_per_day","added_at","displayName","followers","postsPerWeek","engagementRate","topPost","addedAt"]},"MarketingIntegration":{"type":"object","properties":{"provider":{"type":"string","enum":["meta","x","ga4","search_console","resend","sendgrid","supabase_email"]},"status":{"type":"string"},"config":{},"updated_at":{"type":["string","null"]}},"required":["provider","status","updated_at"]},"MarketingBrand":{"type":"object","properties":{"logoUrl":{"type":["string","null"]},"primaryColor":{"type":["string","null"]},"secondaryColor":{"type":["string","null"]},"ngWords":{"type":"array","items":{"type":"string"}},"defaultHashtags":{"type":"array","items":{"type":"string"}},"defaultSignature":{"type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["logoUrl","primaryColor","secondaryColor","ngWords","defaultHashtags","defaultSignature","updatedAt"]},"MarketingAsset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"type":"string","enum":["image","video","gif","logo","icon","banner"]},"mime_type":{"type":["string","null"]},"r2_key":{"type":"string"},"public_url":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"byte_size":{"type":["integer","null"]},"duration_seconds":{"type":["integer","null"]},"tags":{"type":"array","items":{"type":"string"}},"alt_text":{"type":["string","null"]},"usage_count":{"type":"integer"},"uploaded_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","name","kind","mime_type","r2_key","public_url","width","height","byte_size","duration_seconds","tags","alt_text","usage_count","uploaded_by","created_at"]},"MarketingNotification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","enum":["x_post_failed","newsletter_failed","campaign_start","campaign_end","automation_fired","integration_error"]},"title":{"type":"string"},"body":{"type":["string","null"]},"severity":{"type":"string","enum":["info","warning","error","success"]},"read_at":{"type":["string","null"]},"link":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","user_id","kind","title","body","severity","read_at","link","created_at"]},"MarketingActivityLog":{"type":"object","properties":{"id":{"anyOf":[{"type":"integer"},{"type":"string"}]},"user_id":{"type":["string","null"],"format":"uuid"},"action":{"type":"string"},"target_kind":{"type":["string","null"]},"target_id":{"type":["string","null"]},"detail":{},"created_at":{"type":"string"}},"required":["id","user_id","action","target_kind","target_id","created_at"]},"MarketingCampaign":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"objective":{"type":["string","null"]},"status":{"type":"string"},"start_date":{"type":"string"},"end_date":{"type":["string","null"]},"budget_minor":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"channels":{"type":"array","items":{"type":"string"}},"target_kpis":{},"tags":{"type":"array","items":{"type":"string"}},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","objective","status","start_date","end_date","budget_minor","channels","tags","created_by","created_at","updated_at"]},"MarketingCampaignItem":{"type":"object","properties":{"campaign_id":{"type":"string","format":"uuid"},"item_kind":{"type":"string","enum":["instagram_post","x_post","article","ad","newsletter"]},"item_id":{"type":"string"},"linked_at":{"type":"string"}},"required":["campaign_id","item_kind","item_id","linked_at"]},"MarketingCampaignDetail":{"allOf":[{"$ref":"#/components/schemas/MarketingCampaign"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCampaignItem"}}},"required":["items"]}]},"MarketingCampaignMetrics":{"type":"object","properties":{"campaign_id":{"type":"string","format":"uuid"},"totals":{"type":"object","properties":{"articles":{"type":"integer"},"ads":{"type":"integer"},"x_posts":{"type":"integer"},"instagram_posts":{"type":"integer"},"newsletters":{"type":"integer"}},"required":["articles","ads","x_posts","instagram_posts","newsletters"]},"pv":{"type":"integer"},"ad_impressions":{"type":"integer"},"ad_clicks":{"type":"integer"},"ctr":{"type":"number"},"x_impressions":{"type":"integer"},"x_likes":{"type":"integer"},"newsletters_sent":{"type":"integer"}},"required":["campaign_id","totals","pv","ad_impressions","ad_clicks","ctr","x_impressions","x_likes","newsletters_sent"]},"MarketingAnalyticsSummary":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"totalPv":{"type":"integer"},"totalImpressions":{"type":"integer"},"totalClicks":{"type":"integer"},"ctr":{"type":"number"},"newsletters_sent":{"type":"integer"},"posts_published":{"type":"integer"},"campaigns_active":{"type":"integer"}},"required":["from","to","totalPv","totalImpressions","totalClicks","ctr","newsletters_sent","posts_published","campaigns_active"]},"MarketingAnalyticsArticleRow":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":["string","null"]},"title":{"type":"string"},"category":{"type":["string","null"]},"author_name":{"type":["string","null"]},"published_at":{"type":["string","null"]},"pv":{"type":"integer"},"pv_diff_pct":{"type":"number"}},"required":["id","slug","title","category","author_name","published_at","pv","pv_diff_pct"]},"MarketingAnalyticsAdRow":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"start_date":{"type":["string","null"]},"end_date":{"type":["string","null"]},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"ctr":{"type":"number"},"days_total":{"type":"integer"},"days_elapsed":{"type":"integer"},"progress":{"type":"number"},"daily":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"clicks":{"type":"integer"}},"required":["date","clicks"]}}},"required":["id","name","status","start_date","end_date","impressions","clicks","ctr","days_total","days_elapsed","progress","daily"]},"MarketingAnalyticsCrossChannelRow":{"type":"object","properties":{"channel":{"type":"string"},"posts":{"type":"integer"},"impressions":{"type":"integer"},"likes":{"type":"integer"},"clicks":{"type":"integer"},"pv":{"type":"integer"}},"required":["channel","posts","impressions","likes","clicks","pv"]},"MarketingContentPoolItem":{"type":"object","properties":{"kind":{"type":"string","enum":["article","instagram_post","x_post","ad","newsletter","asset"]},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"meta":{"type":"object","additionalProperties":{}}},"required":["kind","id","title","status","thumbnail_url","updated_at"]},"MarketingCalendarEvent":{"type":"object","properties":{"kind":{"type":"string","enum":["x_post","article","ad","newsletter","instagram_post"]},"id":{"type":"string"},"title":{"type":"string"},"status":{"type":["string","null"]},"channel":{"type":"string","enum":["x","article","ad","newsletter","instagram"]},"at":{"type":"string"}},"required":["kind","id","title","status","channel","at"]},"MarketingCalendarDay":{"type":"object","properties":{"date":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCalendarEvent"}}},"required":["date","items"]},"MarketingCalendarResponse":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"days":{"type":"array","items":{"$ref":"#/components/schemas/MarketingCalendarDay"}}},"required":["from","to","days"]},"MarketingArticleCategory":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"description":{"type":["string","null"]},"sort_order":{"type":"integer"},"is_deleted":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["code","label","description","sort_order","is_deleted","created_at","updated_at"]},"OwnerParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"status":{"type":["string","null"]},"total_spaces":{"type":["integer","null"]},"structure":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","name","address","status","total_spaces","structure","created_at"]},"OwnerSearchParkingLot":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"has_owner":{"type":"boolean"}},"required":["id","name","address","lat","lng","has_owner"]},"OwnerApplication":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":["string","null"],"format":"uuid"},"owner_name":{"type":"string"},"owner_email":{"type":"string"},"company_name":{"type":["string","null"]},"parking_lot_id":{"type":"string","format":"uuid"},"proof_asset_id":{"type":["string","null"],"format":"uuid"},"additional_notes":{"type":["string","null"]},"status":{"type":"string"},"reviewed_at":{"type":["string","null"]},"reject_reason":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","owner_id","owner_name","owner_email","company_name","parking_lot_id","proof_asset_id","additional_notes","status","reviewed_at","reject_reason","created_at"]},"OwnerReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"user_name":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"status":{"type":"string"},"owner_reply":{"type":["string","null"]},"owner_replied_at":{"type":["string","null"]},"owner_replied_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parking_lot":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"required":["id","parking_lot_id","user_name","rating","comment","status","owner_reply","owner_replied_at","owner_replied_by","created_at","updated_at","parking_lot"]},"OwnerBoost":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"parking_lot_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"credit_per_impression":{"type":["integer","null"]},"daily_budget":{"type":["integer","null"]},"total_budget":{"type":["integer","null"]},"started_at":{"type":["string","null"]},"ended_at":{"type":["string","null"]},"impressions":{"type":["integer","null"]},"clicks":{"type":["integer","null"]},"sessions":{"type":["integer","null"]},"status":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","owner_id","parking_lot_id","name","credit_per_impression","daily_budget","total_budget","started_at","ended_at","impressions","clicks","sessions","status","created_at"]},"OwnerCreditTxn":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"type":{"type":"string"},"amount":{"type":"integer"},"balance_after":{"type":"integer"},"description":{"type":["string","null"]},"boost_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string"}},"required":["id","owner_id","type","amount","balance_after","description","boost_id","created_at"]}},"parameters":{}},"paths":{"/v1/marketing/articles":{"get":{"tags":["admin","articles"],"summary":"記事一覧（下書き含む全ステータス + タイトル検索）","description":"### 用途\n管理者向けに `articles` テーブルの記事を一覧する。公開済み（`published`）だけでなく\n下書き（`draft`）・予約・非公開も含めた全ステータスを返し、タイトル ILIKE / カテゴリ /\nステータスで絞り込める。並びは `updated_at DESC`。\n\n### 管理者ポータルでの使用タイミング\n- コンテンツ管理 > 記事管理 のメイン一覧テーブルを開いたとき\n- ステータスタブ（公開中 / 下書き / 予約）切替時\n- 検索ボックスへのキーワード入力（タイトル ILIKE 部分一致）\n- カテゴリドロップダウンでフィルタしたとき\n\n### 認証・認可\n`requireAdmin`。管理者ホワイトリストに登録された Auth ユーザーのみアクセス可。\n\n### 挙動・制約\n- ページング: `PageQuerySchema`（page / limit）。`pgRange` で OFFSET/LIMIT に変換\n- `total` は別 COUNT クエリで返却（同一フィルタで集計）\n- ソフトデリート列は持たないため物理削除前提\n\n### 関連\n- `POST /v1/admin/articles` — 新規作成\n- `PATCH /v1/admin/articles/{id}` — 部分更新\n- `DELETE /v1/admin/articles/{id}` — 物理削除","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"category","in":"query"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminArticle"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","articles"],"summary":"記事を作成","description":"### 用途\n新規記事を `articles` テーブルに INSERT する。本文・カテゴリ・タグ・サムネイル URL 等を\nボディで自由に渡す（`passthrough`）。`status` を `draft` で作って後から `published` に\n切り替える運用を想定。\n\n### 管理者ポータルでの使用タイミング\n- コンテンツ管理 > 記事管理 の「新規作成」ボタン押下時\n- 一覧から複製（コピー＆編集）した直後の保存時\n- 外部 CMS / インポーター経由の自動取り込み\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- ボディは `passthrough` のため列名と一致するキーだけ採用される（タイポはそのまま列指定）\n- 作成成功時に `recordAdminActivityBestEffort` で `article.create` 監査ログを残す\n- 画像アップロードは別 API（assets 系）で先に行い、`thumbnail_url` をここで保存\n\n### 関連\n- `GET /v1/admin/articles` — 一覧\n- `PATCH /v1/admin/articles/{id}` — 公開ステータス変更等","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminArticle"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/articles/{id}":{"patch":{"tags":["admin","articles"],"summary":"記事を更新","description":"### 用途\n既存記事を部分更新する。リクエストボディに含まれた列だけが UPDATE 対象（postgres.js の\n`sql(body)` ヘルパー）。空ボディの場合は現行レコードをそのまま返す。\n\n### 管理者ポータルでの使用タイミング\n- 記事編集画面の「保存」ボタン押下時\n- 一覧画面でステータスをインライン切替したとき（draft ↔ published）\n- カテゴリ・タグ一括編集モーダルからのコミット\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- 公開フラグ: `status = 'published'` のとき `published_at` をボディ側で同時にセットする運用\n- 該当 ID が無い場合は `not_found`（404）\n- 監査ログ `article.update` に diff（送信ボディ）を記録\n\n### 関連\n- `POST /v1/admin/articles` — 新規作成\n- `DELETE /v1/admin/articles/{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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminArticle"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","articles"],"summary":"記事を削除","description":"### 用途\n記事を物理削除する（`DELETE FROM articles`）。ソフトデリート列は持たないため復元不可。\n誤削除を防ぐためポータル側で確認モーダルを必ず挟むこと。\n\n### 管理者ポータルでの使用タイミング\n- 記事編集画面の「削除」ボタン → 確認モーダル承認時\n- 一覧画面の行アクションメニュー「削除」\n- 複数選択での一括削除（ループ呼び出し）\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- 物理削除（履歴テーブルや関連テーブルへの cascade は DB スキーマに依存）\n- 監査ログ `article.delete` にタイトルを残す（削除前に SELECT して保存）\n- 存在しない ID でも 204 を返す（冪等）\n\n### 関連\n- `PATCH /v1/admin/articles/{id}` — ソフトに非公開化したい場合は `status` を変える","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/ads":{"get":{"tags":["admin","ads"],"summary":"広告一覧（管理 + 名前検索）","description":"### 用途\nアプリ内バナー広告 (`ads`) の管理一覧。配信ステータス・名前 ILIKE で絞り込みでき、\nインプレッション / クリック数も同時に返すので運用ダッシュボードで KPI 確認に使える。\n\n### 管理者ポータルでの使用タイミング\n- コンテンツ管理 > 広告管理 のメイン一覧テーブル\n- ステータスタブ（配信中 / 停止 / 期限切れ）切替時\n- 名前検索ボックス入力時\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- ページング: `PageQuerySchema`\n- 並び順: `updated_at DESC`\n- 配信期間（`start_date` / `end_date`）はここでは絞り込まず、画面側で表示判定\n\n### 関連\n- `POST /v1/admin/ads` — 広告作成\n- `PATCH /v1/admin/ads/{id}` — 配信停止 / 期間延長","parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"1 はじまりのページ番号","examples":["1"]},"required":false,"name":"page","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$","description":"1 ページあたりの件数（最大 2000）","examples":["20"]},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminAd"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","ads"],"summary":"広告を作成","description":"### 用途\n新規広告枠を `ads` テーブルに登録する。バナー画像 URL・遷移先 URL・配信期間・配置位置\n（placement）等を含むレコードを作る。\n\n### 管理者ポータルでの使用タイミング\n- 広告管理 > 「新規広告」ボタン\n- 既存広告を複製して新キャンペーン作成時\n- スポンサー紐付け広告の自動生成（連携導線）\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- ボディ `passthrough` で列名と一致するキーだけ採用\n- バナー画像は assets 系で先にアップ → `banner_url` を渡す\n- 監査ログ `ad.create` に status / placement を記録\n- 初期 `impressions` / `clicks` は DB のデフォルト 0\n\n### 関連\n- `GET /v1/admin/ads` — 一覧\n- `PATCH /v1/admin/ads/{id}` — 配信制御","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAd"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/ads/{id}":{"patch":{"tags":["admin","ads"],"summary":"広告を更新","description":"### 用途\n広告レコードを部分更新する。配信ステータス切替・配信期間延長・遷移 URL 変更などに用いる。\n空ボディなら現行値をそのまま返却。\n\n### 管理者ポータルでの使用タイミング\n- 広告編集画面の「保存」\n- 一覧でステータスをインライン切替したとき（active ↔ paused）\n- 緊急停止フラグの即時反映\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- 該当 ID なしは `not_found`（404）\n- 監査ログ `ad.update` に diff（送信ボディ）を記録\n- インプレッション / クリック数はここでは触らず、配信側集計バッチで更新する想定\n\n### 関連\n- `DELETE /v1/admin/ads/{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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAd"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","ads"],"summary":"広告を削除","description":"### 用途\n広告を物理削除する。ソフトデリート列は無いため、停止して残したい場合は\n`PATCH` で `status` を `paused` 等に変更すること。\n\n### 管理者ポータルでの使用タイミング\n- 広告編集画面の「削除」確認モーダル承認後\n- 期限切れ広告のクリーンアップ運用\n\n### 認証・認可\n`requireAdmin`。\n\n### 挙動・制約\n- 物理削除（DELETE）。配信実績（impressions/clicks）も一緒に消える\n- 監査ログ `ad.delete` に削除直前の name を記録\n- 存在しない ID でも 204（冪等）\n\n### 関連\n- `PATCH /v1/admin/ads/{id}` — ステータスで非配信化（履歴を残したい場合）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/categories":{"get":{"tags":["admin","instagram"],"summary":"スライドカテゴリ一覧","description":"### 用途\nInstagram スライドテンプレートを分類する **スライドカテゴリ**（cover / spot_detail / ranking / cta など）の一覧を返す。\nテンプレート登録画面のカテゴリセレクター、フィルタリング、AI 自動生成時の種別判定に使う。\nソフトデリート済み（`is_deleted = 1`）は含まれず、`sort_order` 昇順で返す。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > テンプレート管理画面の「カテゴリ」セレクター描画時\n- スライドカテゴリ管理画面（CRUD）を開いたとき\n- 投稿一括生成ウィザードでテンプレ種別を選ばせるとき\n\n### 認証・認可\n`requireAdmin` 必須（管理者ポータルからのみアクセス可）。追加の権限スコープは不要。\n\n### 挙動・制約\nCloudflare D1（`INSTAGRAM_DB` binding）の `ig_slide_categories` テーブルを SELECT。\nParky Supabase とは完全分離されたデータストア。\n\n### 関連\n- `POST /v1/admin/instagram/categories` — 新規カテゴリ追加\n- `PATCH /v1/admin/instagram/categories/{code}` — ラベル/プレフィックス更新\n- `DELETE /v1/admin/instagram/categories/{code}` — ソフトデリート","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgSlideCategory"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"スライドカテゴリ作成","description":"### 用途\n新しいスライドカテゴリを登録する。`code`（種別キー）はテンプレートとリンクする一意 ID で、\n`label`（日本語表示名）、`prefix`（命名規則のヒント）、`sort_order` を指定する。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > スライドカテゴリ管理 > 「+ 追加」ダイアログの保存時\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`ig_slide_categories` に INSERT。`sort_order` 未指定時は 50 で挿入。`code` は UNIQUE 想定（重複時は SQLite が制約違反を返す）。\n\n### 関連\n- `GET /v1/admin/instagram/categories` — 一覧取得\n- `PATCH /v1/admin/instagram/categories/{code}` — 更新","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"prefix":{"type":"string"},"sort_order":{"type":"integer"}},"required":["code","label"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlideCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/categories/{code}":{"patch":{"tags":["admin","instagram"],"summary":"スライドカテゴリ更新","description":"### 用途\n既存スライドカテゴリのラベル / プレフィックス / 並び順を部分更新する。`code` は変更不可。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > スライドカテゴリ管理 > 行のインライン編集 / モーダル保存時\n- ドラッグ&ドロップで `sort_order` を入れ替えたとき\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n送信されたフィールドだけ動的に SET 句を組み立てて UPDATE。`updated_at` は自動更新。\n対象が存在しない場合は 404 `not_found`。\n\n### 関連\n- `GET /v1/admin/instagram/categories` — 一覧\n- `DELETE /v1/admin/instagram/categories/{code}` — 削除","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"prefix":{"type":["string","null"]},"sort_order":{"type":"integer"}}}}}},"responses":{"200":{"description":"更新済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlideCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"スライドカテゴリ削除（ソフトデリート）","description":"### 用途\nスライドカテゴリを論理削除する（`is_deleted = 1`）。物理削除はせず、過去のテンプレが参照している\n履歴コードを壊さないようにする。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > スライドカテゴリ管理 > 行のゴミ箱アイコンタップ\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`UPDATE ig_slide_categories SET is_deleted = 1` のみ。テンプレ側の `slide_type` は影響を受けない。\n存在しない `code` を指定しても 200 を返す（冪等）。\n\n### 関連\n- `POST /v1/admin/instagram/categories` — 同じ code で再作成は不可（必要なら別 code で）","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"responses":{"200":{"description":"削除完了","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/post-categories":{"get":{"tags":["admin","instagram"],"summary":"投稿カテゴリ一覧","description":"### 用途\n投稿（キャンペーン）単位の分類タグ「**投稿カテゴリ**」一覧を返す。\n「特集」「リール」「キャンペーン」などのジャンル分けで、ポータル側のフィルタや色分け表示に使う。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 投稿一覧画面のカテゴリフィルター\n- 投稿作成 / 編集モーダルの「カテゴリ」セレクター\n- 投稿カテゴリ管理画面の表示時\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\nD1 `ig_post_categories` から `is_deleted = 0` のレコードを `sort_order` 昇順で返す。\n`color` は HEX カラーコード（バッジの色付けに使用）。\n\n### 関連\n- `POST /v1/admin/instagram/post-categories` — 新規追加\n- `PATCH /v1/admin/instagram/posts/{id}` — `post_category_code` で投稿に紐付け","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgPostCategory"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"投稿カテゴリ作成","description":"### 用途\n新しい投稿カテゴリ（特集・リール・キャンペーン等）を登録する。\n`code` は投稿レコードの `post_category_code` と紐付く。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > 投稿カテゴリ管理 > 「+ 追加」モーダル保存\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`ig_post_categories` に INSERT。`color` は HEX 文字列（例: `#7c5cfc`）でバッジ色として使う。\n`sort_order` 未指定時は 50。\n\n### 関連\n- `GET /v1/admin/instagram/post-categories` — 一覧","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"label":{"type":"string","minLength":1},"color":{"type":"string"},"sort_order":{"type":"integer"}},"required":["code","label"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgPostCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/post-categories/{code}":{"patch":{"tags":["admin","instagram"],"summary":"投稿カテゴリ更新","description":"### 用途\n投稿カテゴリのラベル / 色 / 並び順を部分更新する。`code` は不変。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > 投稿カテゴリ管理 > インライン編集 / 並び替え\n- カラーピッカーでバッジ色を変更したとき\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n送信されたフィールドのみ動的 UPDATE。`updated_at` 自動更新。対象が存在しない場合 404。\n\n### 関連\n- `DELETE /v1/admin/instagram/post-categories/{code}` — ソフトデリート","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"color":{"type":["string","null"]},"sort_order":{"type":"integer"}}}}}},"responses":{"200":{"description":"更新済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgPostCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"投稿カテゴリ削除（ソフトデリート）","description":"### 用途\n投稿カテゴリを論理削除する（`is_deleted = 1`）。既存の投稿が `post_category_code` で参照していても\n履歴ラベルが残るよう物理削除はしない。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool > 設定 > 投稿カテゴリ管理 > 行のゴミ箱アイコン\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`UPDATE ig_post_categories SET is_deleted = 1`。冪等（存在しなくても 200）。\n\n### 関連\n- `GET /v1/admin/instagram/post-categories` — 一覧（ソフトデリート除外）","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"responses":{"200":{"description":"削除","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/tags":{"get":{"tags":["admin","instagram"],"summary":"タグ一覧","description":"### 用途\n投稿 / テンプレートに付けられる **タグ** の一覧を返す。`usage_count` 降順でソートされ、\nよく使われるタグから提示される。Instagram のハッシュタグとは別の管理用ラベル。\n\n### 管理者ポータルでの使用タイミング\n- 投稿編集画面のタグ入力欄のオートコンプリート / サジェスト\n- テンプレート編集画面のタグ入力欄\n- タグ管理画面（CRUD）の表示\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`SELECT * FROM ig_tags ORDER BY usage_count DESC, name ASC`。\n`usage_count` は `ig_campaign_tags` / `ig_template_tags` のリンク数で集計され、PUT 系で増減する。\n\n### 関連\n- `POST /v1/admin/instagram/tags` — 新規追加（既存名なら既存返却）\n- `PUT /v1/admin/instagram/posts/{id}/tags` — 投稿への紐付け","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgTag"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"タグ作成（既存 name があれば既存を返す）","description":"### 用途\n新しいタグを作成する。同じ `name` が既存なら新規作成せず既存レコードをそのまま返す（upsert 的挙動）。\n先頭の `#` は自動で除去、前後空白も trim する。\n\n### 管理者ポータルでの使用タイミング\n- 投稿 / テンプレ編集画面のタグ入力でリスト未存在の名前が入力されたとき\n- タグ管理画面の「+ 追加」モーダル\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`name` は最大 40 文字。`color` は HEX。重複名は SELECT で既存検出 → 即返却（id を変えない）。\n新規作成時は `crypto.randomUUID()` で id を発行。\n\n### 関連\n- `PUT /v1/admin/instagram/posts/{id}/tags` — 紐付け差分更新で usage_count を増減","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":40},"color":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"作成 or 既存","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgTag"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/tags/{id}":{"patch":{"tags":["admin","instagram"],"summary":"タグ更新","description":"### 用途\nタグ名 / 色を部分更新する。`usage_count` はこの API では変更しない（リンク差分で自動）。\n\n### 管理者ポータルでの使用タイミング\n- タグ管理画面の名前リネーム / カラーピッカー変更\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n送信フィールドのみ動的 SET。空ボディは 400 `bad_request`。対象が存在しない場合 404。\n\n### 関連\n- `DELETE /v1/admin/instagram/tags/{id}` — タグ削除（物理削除）","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"更新済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgTag"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"タグ削除","description":"### 用途\nタグを **物理削除** する（カテゴリと違いソフトデリートではない）。リンクテーブル\n（`ig_campaign_tags` / `ig_template_tags`）には FK CASCADE を仮定し、紐付け解除は DB 側で連動する想定。\n\n### 管理者ポータルでの使用タイミング\n- タグ管理画面で行のゴミ箱アイコン\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`DELETE FROM ig_tags WHERE id = ?`。冪等。\n\n### 関連\n- `GET /v1/admin/instagram/tags` — 一覧","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"削除","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/tags":{"put":{"tags":["admin","instagram"],"summary":"投稿のタグ一括設定","description":"### 用途\n投稿（キャンペーン）に紐付くタグを **set 操作で一括上書き** する。送られた `tag_ids` を真として、\nリンクテーブル `ig_campaign_tags` の差分を計算し追加 / 削除。タグの `usage_count` も整合性を保って増減。\n\n### 管理者ポータルでの使用タイミング\n- 投稿編集画面のタグマルチセレクト変更時\n- 投稿一括生成完了後の自動タグ付与\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n現在のリンクと新セットの diff（toAdd / toRemove）を D1 `batch()` で原子的に適用。\n`INSERT OR IGNORE` + `UPDATE ig_tags SET usage_count = ...` を交互に組み立て、\n`MAX(0, usage_count - 1)` で負値を防ぐ。\n\n### 関連\n- `GET /v1/admin/instagram/tags` — 利用可能タグ一覧\n- `PUT /v1/admin/instagram/templates/{id}/tags` — テンプレ側の同等エンドポイント","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tag_ids":{"type":"array","items":{"type":"string"}}},"required":["tag_ids"]}}}},"responses":{"200":{"description":"更新後タグ一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgTag"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/templates/{id}/tags":{"put":{"tags":["admin","instagram"],"summary":"テンプレートのタグ一括設定","description":"### 用途\nスライドテンプレートに紐付くタグを set 操作で一括上書きする。\n投稿側と同じ差分計算ロジックでリンクテーブル `ig_template_tags` を更新。\n\n### 管理者ポータルでの使用タイミング\n- テンプレート編集画面のタグマルチセレクト変更時\n- テンプレ一覧でのインラインタグ編集\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`setEntityTags(db, 'template', ...)` で D1 `batch()` 内に INSERT OR IGNORE / DELETE と\n`ig_tags.usage_count` の +1 / -1 を組み込んで原子的に実行。\n\n### 関連\n- `PUT /v1/admin/instagram/posts/{id}/tags` — 投稿側の同等エンドポイント","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"tag_ids":{"type":"array","items":{"type":"string"}}},"required":["tag_ids"]}}}},"responses":{"200":{"description":"更新後タグ一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgTag"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/post-templates":{"get":{"tags":["admin","instagram"],"summary":"投稿テンプレート一覧","description":"### 用途\n**投稿テンプレート**（複数のスライドテンプレを束ねた雛形）の一覧を返す。\n「カバー → スポット詳細×N → ランキング → CTA」のような定型ストーリーを 1 レコードに保持し、\n`slide_refs` JSON で構成される。\n\n### 管理者ポータルでの使用タイミング\n- 投稿一括生成ウィザードの「投稿テンプレを選ぶ」セレクター\n- 投稿テンプレート管理画面\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`SELECT * FROM ig_post_templates ORDER BY code ASC`。`slide_refs` は文字列 JSON のまま返す\n（クライアント側で `[{template_id, count}]` 形式に parse する）。\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/generate-from-parking-lots` — テンプレを使った一括生成\n- `POST /v1/admin/instagram/posts/{id}/generate-all` — テンプレを使った AI 全体生成","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgPostTemplate"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"投稿テンプレート作成","description":"### 用途\n新しい投稿テンプレートを登録する。`slide_refs` には\n`[{\"template_id\":\"<uuid>\",\"count\":N}, ...]` の JSON 文字列を渡し、AI 一括生成時の組み立て順 / 枚数を定義する。\n\n### 管理者ポータルでの使用タイミング\n- 投稿テンプレート管理 > 「+ 新規」モーダルの保存\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\nid は `crypto.randomUUID()`。`slide_refs` は文字列保存（D1 に JSON 型は無いため）。\n`code` は識別子として UNIQUE 想定。\n\n### 関連\n- `PATCH /v1/admin/instagram/post-templates/{id}` — slide_refs 編集","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"slide_refs":{"type":"string"}},"required":["code","name","slide_refs"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgPostTemplate"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/post-templates/{id}":{"patch":{"tags":["admin","instagram"],"summary":"投稿テンプレート更新","description":"### 用途\n投稿テンプレートのフィールド（code / name / description / slide_refs）を部分更新する。\n`slide_refs` を編集すると、以後の一括生成で使われるスライド構成が変わる。\n\n### 管理者ポータルでの使用タイミング\n- 投稿テンプレート管理 > 編集モーダル保存\n- スライド構成のドラッグ&ドロップ並び替え保存\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n送信されたフィールドのみ動的 SET。空ボディは 400。`updated_at` 自動更新。404 if not found。\n\n### 関連\n- `DELETE /v1/admin/instagram/post-templates/{id}` — 物理削除","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"slide_refs":{"type":"string"}}}}}},"responses":{"200":{"description":"更新済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgPostTemplate"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"投稿テンプレート削除","description":"### 用途\n投稿テンプレートを物理削除する。既に作成済みの投稿（キャンペーン）には影響しない\n（投稿側にはスライドが INSERT 済みで、テンプレへの FK は持たないため）。\n\n### 管理者ポータルでの使用タイミング\n- 投稿テンプレート管理画面のゴミ箱アイコン\n\n### 認証・認可\n`requireAdmin` 必須。\n\n### 挙動・制約\n`DELETE FROM ig_post_templates WHERE id = ?`。冪等。\n\n### 関連\n- `GET /v1/admin/instagram/post-templates` — 一覧","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"削除完了","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/ai-providers":{"get":{"tags":["admin","instagram"],"summary":"使える AI プロバイダー一覧（セレクター用、最小情報のみ）","description":"### 用途\nInstagram tool で使える AI プロバイダー（OpenAI / Anthropic / Workers AI 等）の最小情報を返す。\nセレクター描画用なので `vault_secret_id` 等のシークレット参照キーは含めない。\n\n### 管理者ポータルでの使用タイミング\n- AI 生成系操作（キャプション生成、スロット自動入力、HTML 修正等）の「プロバイダー選択」セレクター描画時\n- 初期表示時に priority 最高のものをデフォルト選択するため\n\n### 認証・認可\n`requireAdmin` 必須。Parky 共通の `ai_providers` テーブル（Supabase）を読むので Hyperdrive 経由。\n\n### 挙動・制約\n`SELECT id, provider_key, display_name, model_name, is_enabled, priority FROM ai_providers`\n`WHERE is_enabled = true ORDER BY priority DESC`。LLM 呼出本体は `loadProviders()` で\nconfig / vault_secret_id まで含めて別途取得する。\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/generate-caption` — provider_id を指定可能\n- `POST /v1/admin/instagram/slides/{id}/generate-content` — provider_id を指定可能","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"provider_key":{"type":"string"},"display_name":{"type":"string"},"model_name":{"type":"string"},"is_enabled":{"type":"boolean"},"priority":{"type":"integer"}},"required":["id","provider_key","display_name","model_name","is_enabled","priority"]}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/templates/analyze-html":{"post":{"tags":["admin","instagram"],"summary":"既存HTMLをAIが解析し、テンプレート化（{{key}}置換 + slot_schema生成）","description":"### 用途\n既存のスライド HTML を LLM に食わせ、「可変部分」を `{{key}}` プレースホルダに\n置換した *テンプレート HTML* と、各プレースホルダの型定義 (`slot_schema`) を生成する。\n制作済みスライドを量産可能なテンプレートに昇格させる半自動ツール。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool のテンプレート作成画面で「既存 HTML から作る」選択時\n- ダッシュボードに貼った HTML スニペットに対する「テンプレ化」操作\n\n### 認証・認可\n要 `requireAdmin`。LLM 消費があるため `instagram:ai` 権限が望ましい。\n\n### 挙動・制約\n- LLM は `ai_providers` テーブルから取得（`provider_id` / `model` で指定可能、未指定なら優先順位 1 位）\n- 応答 JSON を抽出・検証し、`slot_schema.type` は text/textarea/number/url/image に正規化\n- JSON 不正時は `bad_gateway` (502) を返す\n- 使用量は `ai_usage_logs` に記録される（トークン数・コスト）\n- HTML 構造・CSS・class は完全保持、可変テキスト / 画像 URL / 数値のみ置換\n\n### 関連\n- `POST /v1/admin/instagram/post-templates` — テンプレート新規登録\n- `POST /v1/admin/instagram/templates/{id}/revise` — テンプレートを LLM でリバイズ","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"html":{"type":"string","minLength":10},"slide_type":{"type":"string"},"hint":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["html"]}}}},"responses":{"200":{"description":"解析結果","content":{"application/json":{"schema":{"type":"object","properties":{"html_body":{"type":"string"},"slot_schema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"placeholder":{"type":"string"}},"required":["key","label","type"]}},"sample_content":{"type":"object","additionalProperties":{"type":"string"}}},"required":["html_body","slot_schema","sample_content"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/templates":{"get":{"tags":["admin","instagram"],"summary":"テンプレート一覧","description":"### 用途\nInstagram 投稿に使えるスライド HTML テンプレート (`ig_templates`) の一覧を `sort_order` 昇順で返す。\n\n### 管理者ポータルでの使用タイミング\n- スライド新規作成ダイアログの「テンプレート選択」ドロップダウン\n- テンプレート管理画面の一覧表示\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- D1 (SQLite) の `ig_templates` テーブルから取得（Postgres ではない）\n- `sort_order ASC` でソート\n\n### 関連\n- `POST /v1/admin/instagram/templates` — 新規作成\n- `POST /v1/admin/instagram/templates/analyze-html` — HTML から自動生成","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgTemplate"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"テンプレート作成","description":"### 用途\n新しいスライドテンプレートを登録する。`code`（識別子）・`name`・`slide_type`・\n`html_body`（`{{key}}` プレースホルダ入り）・`slot_schema` などを受け取る。\n\n### 管理者ポータルでの使用タイミング\n- テンプレート作成画面の保存時\n- `/templates/analyze-html` の結果を受けて登録するフロー\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `uses_parking_lot` 未指定なら `slide_type` が `spot_detail` / `ranking` のとき 1 に自動設定\n- ID は サーバー側で UUID 生成\n- D1 (SQLite) の `ig_templates` に INSERT\n\n### 関連\n- `PATCH /v1/admin/instagram/templates/{id}` — 部分更新\n- `POST /v1/admin/instagram/templates/analyze-html` — 既存 HTML の自動テンプレ化","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"name":{"type":"string","minLength":1,"maxLength":100},"slide_type":{"type":"string"},"html_body":{"type":"string","minLength":1},"slot_schema":{"type":"string","default":"[]"},"sample_content":{"type":"string","default":"{}"},"sample_html":{"type":"string","default":""},"sort_order":{"type":"integer","default":0},"uses_parking_lot":{"type":"integer","minimum":0,"maximum":1}},"required":["code","name","slide_type","html_body"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgTemplate"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/templates/{id}":{"patch":{"tags":["admin","instagram"],"summary":"テンプレート更新","description":"### 用途\nテンプレートを部分更新する。指定フィールドのみ SET する動的 UPDATE。\n\n### 管理者ポータルでの使用タイミング\n- テンプレート編集画面 → 保存ボタン\n- アクティブ/非アクティブ切替（`is_active`）\n- 並び替え（`sort_order`）\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- 更新対象は undefined でないフィールドだけ動的に SET 節を構築\n- 全フィールド未指定でも 200（現在値を返す）\n- 存在しない id は `not_found` (404)\n\n### 関連\n- `GET /v1/admin/instagram/templates` — 一覧\n- `POST /v1/admin/instagram/templates/{id}/revise` — LLM リバイズ","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"slide_type":{"type":"string"},"code":{"type":"string"},"html_body":{"type":"string"},"slot_schema":{"type":"string"},"sample_content":{"type":"string"},"sample_html":{"type":"string"},"sort_order":{"type":"integer"},"is_active":{"type":"integer","minimum":0,"maximum":1},"uses_parking_lot":{"type":"integer","minimum":0,"maximum":1}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgTemplate"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"テンプレート削除","description":"### 用途\nテンプレートを削除する。スライドが 1 件でも参照していれば 409 を返し、\n`?force=1` で強制削除（参照スライドも cascade）。\n\n### 管理者ポータルでの使用タイミング\n- テンプレート管理画面の削除ボタン\n- 409 後の確認ダイアログで「それでも削除」選択時に `?force=1` で再送\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- 参照スライド数が >0 かつ `force != '1'` のとき 409 + `{ error, slide_count }` を返す\n- `force=1` で参照スライドも含めて削除（SQLite cascade）\n- 存在しない id でも 204（冪等）\n\n### 関連\n- `GET /v1/admin/instagram/templates` — 一覧","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["0","1"]},"required":false,"name":"force","in":"query"}],"responses":{"204":{"description":"削除成功"},"409":{"description":"使用中のため削除不可","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"slide_count":{"type":"number"}},"required":["error","slide_count"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts":{"get":{"tags":["admin","instagram"],"summary":"キャンペーン一覧","description":"### 用途\nInstagram 投稿キャンペーン (`ig_campaigns`) の一覧を返す。\n一覧でもフロントで live プレビューをサムネ表示できるよう、**1 枚目スライドの**\n`template_id` / `content` / `html_override` / `png_url` と `caption_body` を一度の\nクエリで JOIN 取得する。\n\n### 管理者ポータルでの使用タイミング\n- Instagram tool のトップ画面（投稿一覧）\n- ステータス別絞り込み（draft / scheduled / published 等）\n- ダッシュボードの「直近の投稿」ウィジェット\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- D1 (SQLite) の `ig_campaigns` から取得\n- ページング: `limit` 上限 200（未指定時 50）、`offset`\n- ソート: `created_at` 降順\n\n### 関連\n- `POST /v1/admin/instagram/posts` — 新規作成\n- `GET /v1/admin/instagram/posts/{id}` — 詳細（slides + caption を含む）","parameters":[{"schema":{"type":"string"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/IgCampaign"}},"total":{"type":"number"}},"required":["items","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"キャンペーン作成","description":"### 用途\n新しい Instagram 投稿キャンペーン（= 複数スライド + キャプションのコンテナ）を作成する。\n`code`（識別子）・`title`（表示名）・テーマ・エリア・参考資料（source_material）などを受け付ける。\n\n### 管理者ポータルでの使用タイミング\n- 「新規投稿を作成」ボタン押下\n- テーマ・エリア・種別（`post_category_code`）を指定するウィザード開始時\n\n### 認証\n要 `requireAdmin`。`created_by` に操作者 `adminId` が自動で入る。\n\n### 挙動・制約\n- ID は UUID で自動生成、`created_at` / `updated_at` に ISO timestamp を記録\n- スライドは別途 `POST /posts/{id}/slides` で追加\n- 初期 status は `draft`（schema 側のデフォルト）\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/slides` — スライド追加\n- `POST /v1/admin/instagram/posts/{id}/generate-all` — AI で一括生成\n- `POST /v1/admin/instagram/posts/{id}/generate-caption` — キャプション生成","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":20},"title":{"type":"string","minLength":1,"maxLength":200},"theme":{"type":"string"},"area":{"type":"string"},"notes":{"type":"string"},"source_material":{"type":"string"},"post_category_code":{"type":"string"}},"required":["code","title"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgCampaign"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}":{"get":{"tags":["admin","instagram"],"summary":"キャンペーン詳細（slides + caption 含む）","description":"### 用途\n単一キャンペーンの詳細を取得する。`ig_campaigns` + 配下の `ig_slides` 全件 +\n`ig_captions` 1 件をまとめて返す編集画面用の集約 API。\n\n### 管理者ポータルでの使用タイミング\n- 投稿編集画面を開いたとき\n- スライド編集後に最新状態を取り直すとき\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- スライドは `slide_index` 昇順\n- キャプション未生成時は `caption: null`\n- 存在しない id は `not_found` (404)\n\n### 関連\n- `PATCH /v1/admin/instagram/posts/{id}` — キャンペーン部分更新\n- `GET /v1/admin/instagram/posts/{id}/slides` — スライドだけ取得","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"詳細","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/IgCampaign"},{"type":"object","properties":{"slides":{"type":"array","items":{"$ref":"#/components/schemas/IgSlide"}},"caption":{"$ref":"#/components/schemas/IgCaption"}},"required":["slides","caption"]}]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"patch":{"tags":["admin","instagram"],"summary":"キャンペーン更新","description":"### 用途\nキャンペーンのメタデータ（タイトル・テーマ・エリア・ステータス・予約時刻など）を\n部分更新する。ステータス遷移（draft → review → scheduled → published → archived）の\n中心となるエンドポイント。\n\n### 管理者ポータルでの使用タイミング\n- 編集画面の保存ボタン\n- 「下書き保存」「レビュー依頼」「公開予約」などのアクション\n- 参考資料 (`source_material`) 更新\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `updated_at` が自動更新される\n- ステータスは enum (draft / review / scheduled / published / archived)\n- 全フィールド未指定なら現在値を返す（冪等）\n- 実際の Instagram への投稿連携はこの API では行わない（別 job）\n\n### 関連\n- `PATCH /v1/admin/instagram/posts/{id}/caption` — キャプションだけ更新\n- `GET /v1/admin/instagram/posts/{id}/snapshots` — バージョン履歴","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"theme":{"type":["string","null"]},"area":{"type":["string","null"]},"status":{"type":"string","enum":["draft","review","scheduled","published","archived"]},"scheduled_at":{"type":["string","null"]},"notes":{"type":["string","null"]},"source_material":{"type":["string","null"]},"post_category_code":{"type":["string","null"]},"code":{"type":"string"}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgCampaign"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"キャンペーン削除（CASCADE で slides/caption も削除）","description":"### 用途\nキャンペーンを物理削除する。外部キーの CASCADE により `ig_slides` と `ig_captions`\nも同時に削除される。スナップショット (`ig_campaign_snapshots`) も ON DELETE CASCADE。\n\n### 管理者ポータルでの使用タイミング\n- キャンペーン一覧の削除ボタン → 確認モーダル → 実行\n- アーカイブではなく「完全に消す」操作\n\n### 認証\n要 `requireAdmin`。削除権限が必要。\n\n### 挙動・制約\n- CASCADE で子テーブル（slides, caption, snapshots）すべて削除\n- 存在しない id でも 204（冪等）\n- R2 の PNG オブジェクトは GC ジョブが別途掃除\n\n### 関連\n- `PATCH /v1/admin/instagram/posts/{id}` — status='archived' への変更で論理アーカイブ","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"削除成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/slides":{"get":{"tags":["admin","instagram"],"summary":"スライド一覧","description":"### 用途\nキャンペーン配下のスライド (`ig_slides`) を `slide_index` 昇順で返す。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタを開いたとき\n- スライド並び替え後の再取得\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `slide_index` 昇順、存在しなければ空配列\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/slides` — スライド追加\n- `PATCH /v1/admin/instagram/posts/{id}/slides/reorder` — 並び替え","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgSlide"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["admin","instagram"],"summary":"スライド追加","description":"### 用途\nキャンペーンに新しいスライドを追加する。`template_id`・`slide_index`（挿入位置）・\n`content`（`{{key}}` プレースホルダ埋め用の JSON 文字列）を指定。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタ「+スライド追加」ボタン\n- テンプレート選択 → 初期 content 自動埋めで作成\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- D1 `ig_slides` に INSERT、id は UUID 自動生成\n- `content` はデフォルト `{}`（後で PATCH で埋める運用も可）\n- `slide_index` は呼出し側が指定（重複時は自前で reorder が必要）\n\n### 関連\n- `PATCH /v1/admin/instagram/slides/{id}` — content 更新\n- `POST /v1/admin/instagram/slides/{id}/generate-content` — AI でコンテンツ自動生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"template_id":{"type":"string"},"slide_index":{"type":"integer","minimum":0},"content":{"type":"string","default":"{}"}},"required":["template_id","slide_index"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlide"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/slides/reorder":{"patch":{"tags":["admin","instagram"],"summary":"スライド並び替え","description":"### 用途\nスライドの表示順 (`slide_index`) を一括更新する。ドラッグ&ドロップ並び替え後の\n最終配列をまとめて送る。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタでドラッグ&ドロップ並び替え → 手を離したタイミング\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `UNIQUE(campaign_id, slide_index)` 制約を避けるため、一旦負の巨大値にシフト\n  してから本来の index に更新する 2 段階 batch\n- D1 のバッチ API で全 UPDATE を 1 コネクションで実行\n- 204 (No Content) を返す\n\n### 関連\n- `GET /v1/admin/instagram/posts/{id}/slides` — 並び替え後の再取得","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slide_index":{"type":"integer"}},"required":["id","slide_index"]}}},"required":["items"]}}}},"responses":{"204":{"description":"更新成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}":{"patch":{"tags":["admin","instagram"],"summary":"スライド内容更新","description":"### 用途\nスライドの `content`（プレースホルダ埋め JSON）、`html_override`（テンプレバイパス HTML）、\nPNG 参照などを部分更新する。\n\n### 管理者ポータルでの使用タイミング\n- スライド編集フォームの保存\n- AI 生成結果の適用\n- PNG エクスポート完了後の `png_r2_key` / `png_url` 書き戻し\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- 渡されたフィールドのみ動的 UPDATE\n- `updated_at` を自動更新\n- 存在しない id は 404 `not_found`\n\n### 関連\n- `POST /v1/admin/instagram/slides/{id}/generate-content` — AI 生成\n- `POST /v1/admin/instagram/slides/{id}/revise` — AI リバイズ","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"},"html_override":{"type":["string","null"]},"png_r2_key":{"type":["string","null"]},"png_url":{"type":["string","null"]},"revision_notes":{"type":["string","null"]},"parking_lot_id":{"type":["string","null"],"format":"uuid"}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlide"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["admin","instagram"],"summary":"スライド削除","description":"### 用途\n個別スライドを物理削除する。PNG オブジェクトは R2 に残るが別 GC で掃除。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタの削除ボタン\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- DELETE 1 行。後続スライドの `slide_index` は自動で詰められない（必要ならクライアントで reorder）\n- 存在しない id でも 204（冪等）\n\n### 関連\n- `PATCH /v1/admin/instagram/posts/{id}/slides/reorder` — 並び直し","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"削除成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/apply-parking-lot":{"post":{"tags":["admin","instagram"],"summary":"スライドに駐車場を割当て、情報を content に流し込む","description":"### 用途\nスライド単位で「この駐車場の情報を流し込む」運用。\n`parking_lots` テーブルから name / address / image / tags / 料金 等を取得し、\nテンプレの slot_schema に存在するキー（name, address, photo_url, category, description, price 等）へ決定論マッピングして `content` を更新する。\n\n### 挙動\n- `ig_slides.parking_lot_id` を更新\n- `content` の該当キーを上書き（無いキーは無視）\n- description は LLM に 1-2 文で生成させる（失敗しても slot 更新はする）\n- `html_override` は触らない（override 解除は別 PATCH で）","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_id":{"type":"string","format":"uuid"},"fill_description":{"type":"boolean"},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["parking_lot_id"]}}}},"responses":{"200":{"description":"更新後スライド","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlide"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/duplicate":{"post":{"tags":["admin","instagram"],"summary":"スライドを複製（直後に挿入）","description":"### 用途\n既存スライドと同じ `template_id` / `content` / `html_override` を持つ新規スライドを\n作成し、元スライドの直後に挿入する（以降のスライドは +1 される）。PNG は意図的に複製しない\n（同じ画像が並ぶと混乱するため）。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタの「このスライドを複製」ボタン\n- 似たレイアウトのスライドを量産したいとき\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `UNIQUE(campaign_id, slide_index)` 衝突を避けるため、後続スライドを一時的に負の index\n  に逃してから挿入 → 最終 index に詰め直す 2 段階 batch\n- `revision_notes` は引き継がない（新規は空）\n- 存在しない id は 404 `not_found`\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/slides` — ゼロから新規作成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"複製後の新スライド","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlide"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/upload-png":{"post":{"tags":["admin","instagram"],"summary":"スライド PNG を R2 にアップロード（multipart/form-data）","description":"### 用途\nスライドをブラウザ側でレンダー→ PNG 化した画像を受け取り、R2 にアップロードする。\n同じキーに上書きすることで、再生成しても公開 URL が変わらず CDN キャッシュの再バインドも楽。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタの「PNG を確定」または「自動エクスポート」\n- バッチ書き出しツール\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- R2 キーは `posts/<campaign_code>/slides/<NN>.png`（番号 2 桁ゼロパディング）\n- 公開 URL は `/cdn/ig/<key>?v=<timestamp>` で返す（キャッシュバスタ付き）\n- `ig_slides.png_r2_key` / `png_url` を自動更新\n- `INSTAGRAM_R2` binding が無いと 503\n\n### 関連\n- `PATCH /v1/admin/instagram/slides/{id}/confirm-png` — 既存 R2 オブジェクトを公式採用として記録","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{}}}}}},"responses":{"200":{"description":"アップロード完了","content":{"application/json":{"schema":{"type":"object","properties":{"r2_key":{"type":"string"},"public_url":{"type":"string"}},"required":["r2_key","public_url"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/upload-image":{"post":{"tags":["admin","instagram"],"summary":"コンテンツ画像を R2 にアップロード（multipart/form-data）","description":"### 用途\nスライドのスロット（背景画像・商品写真など）に埋め込む汎用画像を R2 にアップロードする。\nファイル名と投稿コード (`campaign_code`) からタイムスタンプ+ハッシュ付き R2 キーを生成する。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタの画像スロットにドラッグ&ドロップ\n- 外部画像の差し替え導線\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- ファイル名サニタイズ（非 ASCII/危険文字を `_` に置換、40 文字まで）\n- R2 キーにはタイムスタンプ (`YYYYMMDD-HHMMSS`) と 8 文字 UUID ハッシュを含め衝突回避\n- 公開 URL は `/cdn/ig/<key>` で返る\n- `INSTAGRAM_R2` binding が無いと 503\n\n### 関連\n- `POST /v1/admin/instagram/slides/{id}/upload-png` — スライド本体の PNG 保存\n- `POST /v1/admin/instagram/images/detect-sensitive` — センシティブ領域検出","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{}}}}}},"responses":{"200":{"description":"アップロード完了","content":{"application/json":{"schema":{"type":"object","properties":{"r2_key":{"type":"string"},"public_url":{"type":"string"}},"required":["r2_key","public_url"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/confirm-png":{"patch":{"tags":["admin","instagram"],"summary":"PNG アップロード完了を記録","description":"### 用途\nスライドに対応する R2 PNG の `r2_key` と `public_url` を DB に記録する確定用エンドポイント。\n`/upload-png` を使わずに別経路（presigned PUT 直送 / バッチ書き込み）でアップしたケースで使う。\n\n### 管理者ポータルでの使用タイミング\n- 外部ツール連携で PNG 書き出し済みの R2 オブジェクトを DB に紐付ける\n- バックフィルスクリプト\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `ig_slides.png_r2_key` / `png_url` / `updated_at` を UPDATE\n- R2 オブジェクトの存在検証は行わない（呼出し側責任）\n- 存在しない slide は 404 `not_found`\n\n### 関連\n- `POST /v1/admin/instagram/slides/{id}/upload-png` — Worker 経由でのアップロード + 自動確定","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"r2_key":{"type":"string"},"public_url":{"type":"string"}},"required":["r2_key","public_url"]}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IgSlide"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/generate-content":{"post":{"tags":["admin","instagram"],"summary":"LLM がスライドの slot_schema に沿って content を自動生成","description":"### 用途\nスライドのテンプレート `slot_schema`（キーと型定義）を LLM に渡し、参考素材から各スロットの\nテキスト / 画像 URL / 数値を自動生成する。既存 `content` はマージで保持。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタの「AI に書かせる」ボタン\n- 特定フィールドのみ再生成（`target_keys` 指定）\n- 新規スライド作成直後の初期埋め\n\n### 認証\n要 `requireAdmin`。LLM 消費あり。\n\n### 挙動・制約\n- 素材: `source_material` 優先、未指定なら `campaign.source_material` → `campaign.notes` の順でフォールバック\n- `persist_source=true` で campaign に素材を保存（次回デフォルト化）\n- プロバイダ: `ai_providers` から取得、`provider_id` / `model` で明示指定可\n- `ai_usage_logs` に使用量が記録される\n\n### 関連\n- `POST /v1/admin/instagram/slides/{id}/revise` — 既存 content を指示に沿ってリバイズ\n- `POST /v1/admin/instagram/posts/{id}/generate-all` — 全スライド一括生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source_material":{"type":"string"},"persist_source":{"type":"boolean"},"hint":{"type":"string"},"target_keys":{"type":"array","items":{"type":"string"}},"provider_id":{"type":"string"},"model":{"type":"string"}}}}}},"responses":{"200":{"description":"生成された content（対象キーのみ。既存値は保持してマージ）","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"object","additionalProperties":{"type":"string"}}},"required":["content"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/generate-all":{"post":{"tags":["admin","instagram"],"summary":"テンプレ一式から投稿全体を AI が組み立てる","description":"### 用途\nキャンペーンに対して、投稿テンプレート（`post_template_id`）またはテンプレ ID 配列\n（`template_ids`）を元に、複数スライド + content + キャプションを **AI がまとめて生成する**\nオールインワン生成エンドポイント。\n\n### 管理者ポータルでの使用タイミング\n- 新規キャンペーン作成後の「AI で一括生成」ボタン\n- テンプレ変更時の全面再生成\n\n### 認証\n要 `requireAdmin`。LLM 消費量が大きい操作。\n\n### 挙動・制約\n- `spot_count` で生成するスライド枚数を制御（0〜10、既定 5）\n- 既存スライドは削除してから再生成（トランザクション）\n- `ai_usage_logs` に生成量・コストが記録される\n- 失敗時は途中まで生成されたスライドが残る可能性あり（呼出し側でリセット推奨）\n\n### 関連\n- `POST /v1/admin/instagram/slides/{id}/generate-content` — 個別スライド生成\n- `POST /v1/admin/instagram/posts/{id}/generate-from-parking-lots` — 駐車場ベース生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"post_template_id":{"type":"string"},"template_ids":{"type":"array","items":{"type":"string"}},"spot_count":{"type":"integer","minimum":0,"maximum":10},"hint":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}}}}}},"responses":{"200":{"description":"作成完了","content":{"application/json":{"schema":{"type":"object","properties":{"created_slides":{"type":"number"}},"required":["created_slides"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/generate-from-parking-lots":{"post":{"tags":["admin","instagram"],"summary":"選択された駐車場から投稿を一括生成（決定論的マッピング）","description":"### 用途\n駐車場 ID 配列（最大 10 件）から、駐車場情報を Parky Postgres から引いて、\n投稿テンプレートの各スライドに **決定論的にマッピング** して content + PNG 指示を生成する。\n「スポット紹介投稿」を短時間で量産するための専用導線。\n\n### 管理者ポータルでの使用タイミング\n- 駐車場ランキング記事の投稿化\n- 特集エリアの駐車場をピックアップしてスライド化\n- カバー + ランキング + 詳細スライド群を一気に作る\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `parking_lot_ids` は 1〜10 件\n- 駐車場情報（name / address / images / rating / tags 等）を Postgres から bulk fetch\n- ランキング系テンプレは順序も決定論的（配列の並び順がそのまま順位）\n- 既存スライドは削除してから生成\n- 画像は R2 既存参照を流用（新規アップロードは不要）\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/generate-all` — テンプレだけから汎用生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parking_lot_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":20},"post_template_id":{"type":"string"},"cover_title":{"type":"string"},"cover_area":{"type":"string"},"hint":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["parking_lot_ids"]}}}},"responses":{"200":{"description":"作成完了","content":{"application/json":{"schema":{"type":"object","properties":{"created_slides":{"type":"number"},"slide_ids":{"type":"array","items":{"type":"string"}}},"required":["created_slides","slide_ids"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/slides/{id}/revise":{"post":{"tags":["admin","instagram"],"summary":"LLM によるスライド HTML 修正","description":"### 用途\nスライドの HTML（`html_override` または テンプレ `html_body`）を、\n自然言語指示 (`instructions`) に従って LLM が修正する。結果は `html_override` に保存され、\n`revision_notes` に指示内容が記録される。\n\n### 管理者ポータルでの使用タイミング\n- スライドエディタ「AI に修正させる」パネル\n- 「もう少し派手に」「ブランドカラーを強調」などの軽微な調整\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- システムプロンプトで「修正後 HTML のみ」を要求、コードフェンスは除去\n- `html_override` に保存 → テンプレ `html_body` より優先される\n- `ai_usage_logs` に使用量記録\n- 元に戻したい場合は PATCH で `html_override: null` を送る\n\n### 関連\n- `PATCH /v1/admin/instagram/slides/{id}` — html_override の手動編集・クリア","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"instructions":{"type":"string","minLength":1},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["instructions"]}}}},"responses":{"200":{"description":"修正済みHTML","content":{"application/json":{"schema":{"type":"object","properties":{"html":{"type":"string"},"usage":{"type":"object","properties":{"input_tokens":{"type":"number"},"output_tokens":{"type":"number"}},"required":["input_tokens","output_tokens"]}},"required":["html","usage"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/templates/{id}/revise":{"post":{"tags":["admin","instagram"],"summary":"LLM によるテンプレート HTML 修正案（保存せずプレビュー返却）","description":"### 用途\nテンプレートの `html_body`（`{{key}}` プレースホルダ入り）または `sample_html`（完成形）を\nLLM で修正する。**保存せずプレビューのみ返却** — 管理者は差分を見てから手動で採用できる。\n\n### 管理者ポータルでの使用タイミング\n- テンプレート編集画面の「AI 提案」→「差分確認」→「採用 / 破棄」\n- 複数案生成して比較する導線\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `target='template'`: 修正時も `{{key}}` を**絶対に削除・改名しない**ようシステムプロンプトで制約\n- `target='sample'`: プレースホルダ制約なし（完成形を直に編集）\n- `current_html` 指定でユーザー未保存の修正を LLM に渡せる\n- 保存は別途 `PATCH /v1/admin/instagram/templates/{id}` で\n\n### 関連\n- `PATCH /v1/admin/instagram/templates/{id}` — 修正案を採用して保存","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"instructions":{"type":"string","minLength":1},"target":{"type":"string","enum":["template","sample"],"default":"template"},"current_html":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["instructions"]}}}},"responses":{"200":{"description":"修正案 HTML","content":{"application/json":{"schema":{"type":"object","properties":{"html":{"type":"string"},"usage":{"type":"object","properties":{"input_tokens":{"type":"number"},"output_tokens":{"type":"number"}},"required":["input_tokens","output_tokens"]}},"required":["html","usage"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/generate-caption":{"post":{"tags":["admin","instagram"],"summary":"LLM によるキャプション生成","description":"### 用途\nキャンペーンのタイトル・エリア・スライド content 要約を元に、Instagram 投稿用の\nキャプション本文と推奨ハッシュタグ配列を LLM に生成させる。\n\n### 管理者ポータルでの使用タイミング\n- 投稿編集画面の「AI でキャプション作成」ボタン\n- スライド編集後にキャプションを再生成する導線\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- スライド全件の `content` を収集してコンテキスト化\n- 出力: `{ caption: string, hashtags: string[] }`\n- DB への保存はしない（`PATCH /posts/{id}/caption` で明示的に保存）\n- `ai_usage_logs` に使用量記録\n\n### 関連\n- `PATCH /v1/admin/instagram/posts/{id}/caption` — 生成結果を保存\n- `POST /v1/admin/instagram/posts/{id}/generate-ideas` — タイトル案生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"hint":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}}}}}},"responses":{"200":{"description":"生成されたキャプション","content":{"application/json":{"schema":{"type":"object","properties":{"caption":{"type":"string"},"hashtags":{"type":"array","items":{"type":"string"}}},"required":["caption","hashtags"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/generate-ideas":{"post":{"tags":["admin","instagram"],"summary":"競合分析とコンテンツアイデア生成","description":"### 用途\n競合アカウントの投稿メモや参考 URL から、Parky の Instagram コンテンツ企画案\n（タイトル / コンセプト / フック）を 5〜8 件 LLM に提案させる企画ブレスト用エンドポイント。\n\n### 管理者ポータルでの使用タイミング\n- 新規企画検討画面の「AI にアイデア出しさせる」ボタン\n- 他アカウント（`account_handle`）の URL を貼って類似企画を量産\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `notes` 必須、`area`・`source_url`・`account_handle` 任意\n- 出力は `{ ideas: [{ title, concept, hook }, ...] }`（常に JSON 配列のみ）\n- DB 保存なし（管理者が選んで `POST /v1/admin/instagram/posts` で新規作成する想定）\n- `ai_usage_logs` に使用量記録\n\n### 関連\n- `POST /v1/admin/instagram/posts` — 選んだアイデアでキャンペーン作成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","minLength":1},"area":{"type":"string"},"source_url":{"type":"string"},"account_handle":{"type":"string"},"provider_id":{"type":"string"},"model":{"type":"string"}},"required":["notes"]}}}},"responses":{"200":{"description":"生成されたアイデア一覧","content":{"application/json":{"schema":{"type":"object","properties":{"ideas":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"concept":{"type":"string"},"hook":{"type":"string"}},"required":["title","concept","hook"]}}},"required":["ideas"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/caption":{"patch":{"tags":["admin","instagram"],"summary":"キャプション手動更新","description":"### 用途\nキャンペーンの `ig_captions`（キャプション本文 / ハッシュタグ）を upsert する。\nAI 生成結果の保存、または手動編集内容の反映に使う。\n\n### 管理者ポータルでの使用タイミング\n- 投稿編集画面のキャプションエディタ「保存」\n- `/generate-caption` 生成結果の「この内容で保存」採用時\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `INSERT ... ON CONFLICT(campaign_id) DO UPDATE` で upsert\n- `body` / `hashtags` いずれか未指定なら既存値を COALESCE で保持\n- 1 キャンペーンにつき 1 行（UNIQUE 制約）\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/generate-caption` — AI 生成（保存しない）","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string"},"hashtags":{"type":"string"}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/IgCaption"},{"type":"object"}]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/images/detect-sensitive":{"post":{"tags":["admin","instagram"],"summary":"画像から顔/ナンバープレート候補領域を検出","description":"### 用途\n画像 URL を Cloudflare Workers AI に投げ、**顔 / ナンバープレート候補領域**\nのバウンディングボックスを返す。モザイク / ぼかし処理の初期候補として利用。\n\n### 管理者ポータルでの使用タイミング\n- 画像を投稿に取り込むとき、センシティブ領域を事前検出して編集画面に反映\n- モザイクエディタの「自動検出」ボタン\n\n### 認証\n要 `requireAdmin`。Workers AI 消費あり（`env.AI` binding が必要）。\n\n### 挙動・制約\n- Workers AI は専用の顔 / プレート検出モデルを持たないため、\n  `@cf/facebook/detr-resnet-50`（汎用物体検出）で person / car / truck / bus を検出し、\n  person box の上部 30% を **顔候補**、車の下部 15% 中央を **プレート候補** として返す\n- ヒューリスティック初期候補なので、管理者が**必ず手動で確認・修正**する前提\n- `min_score` で DETR 信頼度フィルタ（既定 0.5）\n- `image_width/height` 指定時は正規化した座標を返す\n\n### 関連\n- `POST /v1/admin/instagram/upload-image` — 検出対象の画像アップロード","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"image_url":{"type":"string","format":"uri"},"image_width":{"type":"integer","exclusiveMinimum":0},"image_height":{"type":"integer","exclusiveMinimum":0},"min_score":{"type":"number","minimum":0,"maximum":1}},"required":["image_url"]}}}},"responses":{"200":{"description":"検出された候補領域（相対座標 0-1）","content":{"application/json":{"schema":{"type":"object","properties":{"boxes":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","enum":["face","plate"]},"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"score":{"type":"number"},"source":{"type":"string"}},"required":["label","x","y","width","height","score","source"]}}},"required":["boxes"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/instagram/posts/{id}/snapshots":{"get":{"tags":["admin","instagram"],"summary":"競合分析スナップショット一覧","description":"### 用途\nキャンペーンに紐づく **競合分析スナップショット** (`ig_competitor_snapshots`) 一覧を返す。\n`/generate-ideas` で生成したアイデアと、その時参照した競合 URL / メモを履歴として保存する。\n\n### 管理者ポータルでの使用タイミング\n- 企画タブの「過去の競合分析」セクション\n- アイデア生成履歴の参照\n\n### 認証\n要 `requireAdmin`。\n\n### 挙動・制約\n- `created_at DESC` 降順（新しいスナップショットが上）\n- スナップショットは `/generate-ideas` 実行時に自動で保存される\n\n### 関連\n- `POST /v1/admin/instagram/posts/{id}/generate-ideas` — 新規スナップショット生成","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IgCompetitorSnapshot"}}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/dashboard/sns-metrics":{"get":{"tags":["marketing","dashboard"],"summary":"Instagram / X フォロワー・エンゲージメント（直近 12 週）","description":"SNS フォロワー数とエンゲージメント率の時系列を返す。Marketing Portal のダッシュボード\nトップに表示する。現状は `sns_follower_snapshots` テーブルが未作成のため、各 platform\nとも followers=0 / series は 12 週分の 0 埋めを返す。\n\nTODO(agent-b): migration 046 以降で `sns_follower_snapshots(platform text, week_starting date,\nfollowers int, engagement_rate numeric)` を作り、cron で週次取得。","responses":{"200":{"description":"SNS 指標","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnsMetricsResponse"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/dashboard/post-calendar":{"get":{"tags":["marketing","dashboard"],"summary":"投稿カレンダー（月単位 × IG/X ステータス別）","description":"指定月の各日について Instagram / X の 下書き・予約・公開 件数を返す。\nIG は D1 `ig_campaigns` に居るためここでは 0 を返す（TODO: Agent B で Postgres mirror を作成）。\nX は `x_posts.status` + `scheduled_at` / `published_at` で集計。","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}$"},"required":true,"name":"year","in":"query"},{"schema":{"type":"string","pattern":"^\\d{1,2}$"},"required":true,"name":"month","in":"query"}],"responses":{"200":{"description":"カレンダー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCalendarResponse"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/dashboard/articles-pv":{"get":{"tags":["marketing","dashboard"],"summary":"記事 PV ランキング + 流入元内訳","description":"公開記事を `view_count` 降順で上位 10 件返す。period は表示用ラベルで、現状フィルタには\n利用しない（articles テーブルに PV の時系列履歴が無いため）。\n\nTODO(ga4-integration): 流入元 / period フィルタは GA4 連携後に実装。現状 sources は全て 0。","parameters":[{"schema":{"type":"string","enum":["yesterday","7d","30d"]},"required":false,"name":"period","in":"query"}],"responses":{"200":{"description":"PV ランキング","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticlesPvResponse"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/dashboard/ads-ctr":{"get":{"tags":["marketing","dashboard"],"summary":"配信中広告の CTR / 残日数","description":"`ads.status = 'active'` の広告について impressions / clicks / CTR と残日数を返す。\ndaily は将来 GA4 / 内部集計連携時に実装するため現状は空配列。","responses":{"200":{"description":"広告 CTR","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdsCtrResponse"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/subscribers":{"get":{"tags":["marketing","newsletter"],"summary":"購読者一覧（status / email 検索 + ページング）","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","enum":["active","unsubscribed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingSubscriber"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","newsletter"],"summary":"購読者を単発追加（upsert）","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"source":{"type":"string"},"lang":{"type":"string"},"meta":{"type":"object","additionalProperties":{}}},"required":["email"]}}}},"responses":{"200":{"description":"追加/更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingSubscriber"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/subscribers/tag-summary":{"get":{"tags":["marketing","newsletter"],"summary":"購読者タグの distinct + 件数（セグメント UI 用）","responses":{"200":{"description":"タグ別の購読者数","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string"},"count":{"type":"integer"}},"required":["tag","count"]}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/subscribers/{id}":{"put":{"tags":["marketing","newsletter"],"summary":"購読者を更新（タグ編集 / 購読解除）","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingSubscriber"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","newsletter"],"summary":"購読者を削除（物理削除）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"削除済み"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/broadcasts":{"get":{"tags":["marketing","newsletter"],"summary":"配信一覧","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","enum":["draft","scheduled","sent","sending","failed"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingBroadcast"}},"page":{"type":"integer","minimum":1},"limit":{"type":"integer","minimum":1},"total":{"type":"integer","minimum":0}},"required":["items","page","limit","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","newsletter"],"summary":"配信を新規作成（status='draft'）","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"subject":{"type":"string"},"body_markdown":{"type":"string"},"segment":{"type":"string"},"engine":{"type":"string"}},"required":["title"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingBroadcast"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/broadcasts/{id}":{"get":{"tags":["marketing","newsletter"],"summary":"配信を単体取得","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/MarketingBroadcast"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","newsletter"],"summary":"配信を部分更新","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingBroadcast"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/broadcasts/{id}/send-test":{"post":{"tags":["marketing","newsletter"],"summary":"テスト配信（現状はログ出力のみのスタブ）","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":{"email":{"type":"string","format":"email"}},"required":["email"]}}}},"responses":{"200":{"description":"ログに記録","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/broadcasts/{id}/schedule":{"post":{"tags":["marketing","newsletter"],"summary":"配信予約（status='scheduled' に更新）","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":{"scheduledAt":{"type":"string","format":"date-time"}},"required":["scheduledAt"]}}}},"responses":{"200":{"description":"予約済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingBroadcast"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/newsletter/broadcasts/{id}/cancel":{"post":{"tags":["marketing","newsletter"],"summary":"配信予約をキャンセル（status='draft' に戻す）","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/MarketingBroadcast"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/accounts":{"get":{"tags":["marketing","x"],"summary":"登録済み X アカウント一覧","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXAccount"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","x"],"summary":"X アカウントを登録（handle + access_token）","description":"X の OAuth 2.0 トークンを `x_accounts.access_token_encrypted` に保存する。\n\naccess_token は lib/crypto.ts の encryptSecret (AES-GCM, env.MARKETING_ENCRYPT_KEY)\nで暗号化してから DB に書き込む。読み出し側 (cron/x-*.ts) は decryptSecret で復号する。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"handle":{"type":"string"},"access_token":{"type":"string"},"tier":{"type":"string","enum":["free","basic","pro"]}},"required":["handle","access_token"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXAccount"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/oauth/start":{"get":{"tags":["marketing","x"],"summary":"X OAuth 2.0 PKCE 開始 URL を返す（スタブ）","responses":{"200":{"description":"OAuth URL","content":{"application/json":{"schema":{"type":"object","properties":{"authorize_url":{"type":"string"},"state":{"type":"string"},"todo":{"type":"string"}},"required":["authorize_url","state","todo"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/oauth/callback":{"get":{"tags":["marketing","x"],"summary":"X OAuth コールバック受け口（スタブ）","parameters":[{"schema":{"type":"string"},"required":false,"name":"code","in":"query"},{"schema":{"type":"string"},"required":false,"name":"state","in":"query"}],"responses":{"200":{"description":"スタブ","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"todo":{"type":"string"}},"required":["ok","todo"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/posts":{"get":{"tags":["marketing","x"],"summary":"投稿一覧（status 絞り込み）","parameters":[{"schema":{"type":"string","enum":["draft","scheduled","publishing","published","failed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":false,"name":"account_id","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXPost"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","x"],"summary":"投稿を新規作成（デフォルト status='draft'）","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"body":{"type":"string"},"thread_of":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"status":{"type":"string"},"scheduled_at":{"type":"string","format":"date-time"}},"required":["account_id","body"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXPost"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/posts/{id}":{"get":{"tags":["marketing","x"],"summary":"投稿を単体取得（全カラム）","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/MarketingXPost"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","x"],"summary":"投稿を部分更新","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXPost"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","x"],"summary":"投稿を削除（物理削除）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/posts/{id}/schedule":{"post":{"tags":["marketing","x"],"summary":"投稿を予約（status='scheduled' に更新）","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":{"scheduledAt":{"type":"string","format":"date-time"}},"required":["scheduledAt"]}}}},"responses":{"200":{"description":"予約済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXPost"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/posts/{id}/publish-now":{"post":{"tags":["marketing","x"],"summary":"投稿を即時公開（status='publishing'）","description":"投稿を即時公開キューに入れる。status を 'publishing' に更新。\n実際の X API 呼び出しは Agent D の cron/x-scheduled-post が拾う。","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/MarketingXPost"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/schedule-rules":{"get":{"tags":["marketing","x"],"summary":"投稿スケジュールルール一覧","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXScheduleRule"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","x"],"summary":"スケジュールルール新規作成","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXScheduleRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/schedule-rules/{id}":{"put":{"tags":["marketing","x"],"summary":"スケジュールルール更新","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXScheduleRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","x"],"summary":"スケジュールルール削除","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/schedule-rules/{id}/toggle":{"patch":{"tags":["marketing","x"],"summary":"スケジュールルールの enabled をトグル","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/MarketingXScheduleRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/listen-rules":{"get":{"tags":["marketing","x"],"summary":"リスニングルール一覧","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXListenRule"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","x"],"summary":"リスニングルール新規作成","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXListenRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/listen-rules/{id}":{"put":{"tags":["marketing","x"],"summary":"リスニングルール更新","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXListenRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","x"],"summary":"リスニングルール削除","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/listen-rules/{id}/toggle":{"patch":{"tags":["marketing","x"],"summary":"リスニングルールの enabled をトグル","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/MarketingXListenRule"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/automation-log":{"get":{"tags":["marketing","x"],"summary":"自動化ルールの発火履歴","parameters":[{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"履歴","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXAutomationLog"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/insights":{"get":{"tags":["marketing","x"],"summary":"X インサイト（スタブ）","responses":{"200":{"description":"インサイト","content":{"application/json":{"schema":{"type":"object","properties":{"followerTrend":{"type":"array","items":{"type":"object","properties":{"day":{"type":"integer"},"followers":{"type":"integer"},"date":{"type":"string"}},"required":["day","followers"]}},"topPosts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"body":{"type":"string"},"date":{"type":"string"},"likes":{"type":"integer"},"reposts":{"type":"integer"},"replies":{"type":"integer"},"impressions":{"type":"integer"}},"required":["body","date","likes","reposts","replies","impressions"]}},"heatmap":{"type":"array","items":{"type":"array","items":{"type":"integer"}}}},"required":["followerTrend","topPosts","heatmap"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/competitors":{"get":{"tags":["marketing","x"],"summary":"競合アカウント一覧","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingXCompetitor"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","x"],"summary":"競合アカウントを追加","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"handle":{"type":"string"},"memo":{"type":"string"}},"required":["handle"]}}}},"responses":{"200":{"description":"追加済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingXCompetitor"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/x/competitors/{handle}":{"delete":{"tags":["marketing","x"],"summary":"競合アカウントを削除","parameters":[{"schema":{"type":"string"},"required":true,"name":"handle","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/integrations":{"get":{"tags":["marketing","integrations"],"summary":"連携一覧（config は機微キーをマスクして返却）","description":"対応プロバイダー `(meta, x, ga4, search-console, resend, sendgrid)` の連携状態を返す。\nDB に行が無い provider は `status='not-configured'` / `config=null` で擬似的に補完して返す\nので、フロントエンドは常に 6 行並べて表示できる。\n\nconfig 内の sensitive なキー（token / secret / password / api_key 等）は '***' に置換される。","responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingIntegration"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/integrations/{provider}":{"put":{"tags":["marketing","integrations"],"summary":"連携設定を upsert","description":"指定 provider の config を UPSERT する。status はボディで指定（省略時は 'connected'）。\nconfig は jsonb 置換（既存値をまるごと差し替え）。","parameters":[{"schema":{"type":"string"},"required":true,"name":"provider","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["connected","not_configured","error","pending"]},"config":{"type":"object","additionalProperties":{}}}}}}},"responses":{"200":{"description":"保存済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingIntegration"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/brand":{"get":{"tags":["marketing","brand"],"summary":"ブランド設定を取得（行が無い場合はデフォルトを返す）","responses":{"200":{"description":"ブランド設定","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingBrand"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","brand"],"summary":"ブランド設定を upsert（id=1 固定シングルトン）","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":["string","null"]},"primaryColor":{"type":["string","null"]},"secondaryColor":{"type":["string","null"]},"ngWords":{"type":["array","null"],"items":{"type":"string"}},"defaultHashtags":{"type":["array","null"],"items":{"type":"string"}},"defaultSignature":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"primary_color":{"type":["string","null"]},"secondary_color":{"type":["string","null"]},"ng_words":{"type":["array","null"],"items":{"type":"string"}},"default_hashtags":{"type":["array","null"],"items":{"type":"string"}},"default_signature":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"保存済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingBrand"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/meta/start":{"get":{"tags":["marketing","oauth"],"summary":"Meta OAuth 認可 URL を発行","description":"Facebook Login ダイアログの URL を返す。フロントエンドは window.open() でポップアップ表示する。","responses":{"200":{"description":"authorize URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/meta/callback":{"get":{"tags":["marketing","oauth"],"summary":"Meta OAuth コールバック","parameters":[{"schema":{"type":"string"},"required":false,"name":"code","in":"query"},{"schema":{"type":"string"},"required":false,"name":"state","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error_description","in":"query"}],"responses":{"200":{"description":"HTML","content":{"text/html":{"schema":{}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/x/start":{"get":{"tags":["marketing","oauth"],"summary":"X OAuth 2.0 (PKCE) 認可 URL を発行","responses":{"200":{"description":"authorize URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/x/callback":{"get":{"tags":["marketing","oauth"],"summary":"X OAuth 2.0 コールバック","parameters":[{"schema":{"type":"string"},"required":false,"name":"code","in":"query"},{"schema":{"type":"string"},"required":false,"name":"state","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error_description","in":"query"}],"responses":{"200":{"description":"HTML","content":{"text/html":{"schema":{}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/search-console/start":{"get":{"tags":["marketing","oauth"],"summary":"Search Console 用 Google OAuth URL を発行","responses":{"200":{"description":"authorize URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/oauth/search-console/callback":{"get":{"tags":["marketing","oauth"],"summary":"Search Console OAuth コールバック","parameters":[{"schema":{"type":"string"},"required":false,"name":"code","in":"query"},{"schema":{"type":"string"},"required":false,"name":"state","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error","in":"query"},{"schema":{"type":"string"},"required":false,"name":"error_description","in":"query"}],"responses":{"200":{"description":"HTML","content":{"text/html":{"schema":{}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/integrations/ga4/upload-sa-json":{"post":{"tags":["marketing","oauth","ga4"],"summary":"GA4 Service Account JSON を投入（疎通確認付き）","description":"サービスアカウント JSON をそのまま body で受け取り、property_id と合わせて\nmarketing.marketing_integrations.config に保存する。GA4 Data API への疎通確認を行い、\n200 が返れば status='connected'、失敗なら status='error' + last_error。\n\nprivate_key は lib/crypto.ts の encryptSecret で AES-GCM 暗号化してから\nJSONB に埋め込む。読み出し側は decryptSecret で復号してから jose.importPKCS8 に渡す。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"property_id":{"type":"string","minLength":1},"service_account":{"type":"object","properties":{"client_email":{"type":"string","format":"email"},"private_key":{"type":"string","minLength":1},"project_id":{"type":"string"},"type":{"type":"string"}},"required":["client_email","private_key"]},"scopes":{"type":"array","items":{"type":"string"}}},"required":["property_id","service_account"]}}}},"responses":{"200":{"description":"接続完了","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"property_id":{"type":"string"},"sa_email":{"type":"string"}},"required":["ok","property_id","sa_email"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/assets":{"get":{"tags":["marketing","assets"],"summary":"アセット一覧（kind / tag / search で絞り込み）","description":"マーケ担当者向けのアセット管理画面用の一覧 API。\n- `kind` で種別フィルタ（image / video / gif / logo / icon / banner）\n- `tag` で単一タグフィルタ（tags @> ARRAY[tag]）\n- `search` で name / alt_text への ILIKE 検索\n- `limit` 省略時は 100（max 500）","parameters":[{"schema":{"type":"string","enum":["image","video","gif","logo","icon","banner"]},"required":false,"name":"kind","in":"query"},{"schema":{"type":"string"},"required":false,"name":"tag","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingAsset"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/assets/upload":{"post":{"tags":["marketing","assets"],"summary":"アップロード用 presigned PUT URL を発行","description":"2 ステップアップロードの 1 段目。レスポンスの `presigned_url` にクライアントが\n直接 `PUT` し、完了後に `POST /assets/:id/confirm` で実サイズ等を確定させる。\n\n`r2_key` は `marketing/{yyyymm}/{uuid}_{sanitized-name}` 形式で衝突しない。\npresigned URL の TTL は 300 秒。","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":300},"kind":{"type":"string","enum":["image","video","gif","logo","icon","banner"]},"mime_type":{"type":"string","minLength":1},"byte_size":{"type":"integer","exclusiveMinimum":0}},"required":["name","kind","mime_type"]}}}},"responses":{"200":{"description":"presigned URL","content":{"application/json":{"schema":{"type":"object","properties":{"asset_id":{"type":"string","format":"uuid"},"presigned_url":{"type":"string","format":"uri"},"r2_key":{"type":"string"},"public_url":{"type":"string"},"expires_in":{"type":"integer"}},"required":["asset_id","presigned_url","r2_key","public_url","expires_in"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/assets/{id}/confirm":{"post":{"tags":["marketing","assets"],"summary":"アップロード完了後のメタデータ確定","description":"PUT 完了後にクライアントから呼び、実サイズ / 寸法 / タグ / alt_text を登録する。","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":{"width":{"type":"integer"},"height":{"type":"integer"},"byte_size":{"type":"integer"},"duration_seconds":{"type":"integer"},"alt_text":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"確定後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingAsset"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/assets/{id}":{"get":{"tags":["marketing","assets"],"summary":"アセット詳細","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/MarketingAsset"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","assets"],"summary":"アセット更新（alt_text / tags / kind / name）","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"},"kind":{"type":"string","enum":["image","video","gif","logo","icon","banner"]},"alt_text":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingAsset"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","assets"],"summary":"アセット削除（メタデータのみ。R2 本体は別途 GC）","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/notifications":{"get":{"tags":["marketing","notifications"],"summary":"通知一覧（自分宛 + 全員向け）","description":"JWT の sub を admins から引いた admin user_id と、user_id IS NULL（全員向け）\nの行を新しい順に返す。`unread=true` で未読のみに絞り込める。","parameters":[{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"unread","in":"query"},{"schema":{"type":"string","enum":["x_post_failed","newsletter_failed","campaign_start","campaign_end","automation_fired","integration_error"]},"required":false,"name":"kind","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingNotification"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/notifications/summary":{"get":{"tags":["marketing","notifications"],"summary":"未読件数サマリー（ヘッダーベル向け）","responses":{"200":{"description":"サマリー","content":{"application/json":{"schema":{"type":"object","properties":{"unread":{"type":"integer"},"total":{"type":"integer"}},"required":["unread","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/notifications/{id}/read":{"post":{"tags":["marketing","notifications"],"summary":"指定通知を既読化","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/MarketingNotification"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/notifications/mark-all-read":{"post":{"tags":["marketing","notifications"],"summary":"すべての未読通知を既読化","responses":{"200":{"description":"既読化件数","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}},"required":["updated"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/notifications/{id}":{"delete":{"tags":["marketing","notifications"],"summary":"通知を削除","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"成功"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/activity":{"get":{"tags":["marketing","activity"],"summary":"監査ログ一覧（target / user / limit で絞り込み）","description":"マーケ操作の監査ログ閲覧。キャンペーン詳細や X ポスト詳細などから\n`target_kind` + `target_id` の組み合わせで絞り込むと、そのオブジェクトに\n対して誰が何をしたかの履歴が取れる。","parameters":[{"schema":{"type":"string"},"required":false,"name":"target_kind","in":"query"},{"schema":{"type":"string"},"required":false,"name":"target_id","in":"query"},{"schema":{"type":"string","format":"uuid"},"required":false,"name":"user_id","in":"query"},{"schema":{"type":"string"},"required":false,"name":"action","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingActivityLog"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/campaigns":{"get":{"tags":["marketing","campaigns"],"summary":"キャンペーン一覧 (status / date range / channel でフィルタ)","parameters":[{"schema":{"type":"string","enum":["planning","active","paused","completed","archived"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"channel","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/MarketingCampaign"},{"type":"object","properties":{"items_count":{"type":"integer"}},"required":["items_count"]}]}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","campaigns"],"summary":"キャンペーン新規作成","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"objective":{"type":"string"},"status":{"type":"string","enum":["planning","active","paused","completed","archived"]},"start_date":{"type":"string"},"end_date":{"type":"string"},"budget_minor":{"type":"number"},"channels":{"type":"array","items":{"type":"string"}},"target_kpis":{"type":"object","additionalProperties":{}},"tags":{"type":"array","items":{"type":"string"}}},"required":["name","start_date"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingCampaign"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/campaigns/{id}":{"get":{"tags":["marketing","campaigns"],"summary":"キャンペーン単体取得 (紐付き items 含む)","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/MarketingCampaignDetail"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","campaigns"],"summary":"キャンペーンを部分更新","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":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingCampaign"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","campaigns"],"summary":"キャンペーンを論理削除 (status='archived')","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"archived"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/campaigns/{id}/items":{"post":{"tags":["marketing","campaigns"],"summary":"コンテンツをキャンペーンに紐付け","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":{"item_kind":{"type":"string","enum":["instagram_post","x_post","article","ad","newsletter"]},"item_id":{"type":"string"}},"required":["item_kind","item_id"]}}}},"responses":{"200":{"description":"紐付け済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingCampaignItem"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/campaigns/{id}/items/{item_kind}/{item_id}":{"delete":{"tags":["marketing","campaigns"],"summary":"キャンペーンからコンテンツ紐付けを解除","parameters":[{"schema":{"type":"string","format":"uuid","examples":["00000000-0000-0000-0000-000000000000"]},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["instagram_post","x_post","article","ad","newsletter"]},"required":true,"name":"item_kind","in":"path"},{"schema":{"type":"string"},"required":true,"name":"item_id","in":"path"}],"responses":{"204":{"description":"unlinked"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/campaigns/{id}/metrics":{"get":{"tags":["marketing","campaigns"],"summary":"キャンペーン metrics 集計 (期間内の PV/IMP/CLK/CTR/投稿数)","description":"紐付け済みの item_kind 別に関連テーブルを UNION ALL で一括 JOIN して集計する。\n現状は PV/IMP/CLK は参照先テーブルの累積値をそのまま合算 (期間フィルタは未実装)。\nGA4 連携後は日次 pv スナップショットと突合して期間内差分を算出する予定。","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/MarketingCampaignMetrics"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/analytics/summary":{"get":{"tags":["marketing","analytics"],"summary":"分析サマリー (期間指定, default=直近30日)","description":"期間内 (from-to) のマーケ KPI を一括で返す。GA4 連携前のため記事 PV は累積値。\nfrom/to を省略した場合は直近 30 日 (UTC)。","parameters":[{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"}],"responses":{"200":{"description":"サマリー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingAnalyticsSummary"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/analytics/articles":{"get":{"tags":["marketing","analytics"],"summary":"記事別 PV 一覧 (period / category フィルタ)","parameters":[{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string"},"required":false,"name":"category","in":"query"}],"responses":{"200":{"description":"記事パフォーマンス","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingAnalyticsArticleRow"}}},"required":["from","to","items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/analytics/ads":{"get":{"tags":["marketing","analytics"],"summary":"広告別 IMP/CLK/CTR と期間進捗","parameters":[{"schema":{"type":"string"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"}],"responses":{"200":{"description":"広告パフォーマンス","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingAnalyticsAdRow"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/analytics/cross-channel":{"get":{"tags":["marketing","analytics"],"summary":"チャネル別サマリー","parameters":[{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"}],"responses":{"200":{"description":"チャネル別","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingAnalyticsCrossChannelRow"}}},"required":["from","to","items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/content-pool":{"get":{"tags":["marketing","content-pool"],"summary":"コンテンツプール横断検索 (article/ad/x_post/newsletter)","parameters":[{"schema":{"type":"string","enum":["article","instagram_post","x_post","ad","newsletter","asset"]},"required":false,"name":"kind","in":"query"},{"schema":{"type":"string"},"required":false,"name":"search","in":"query"},{"schema":{"type":"string"},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"検索結果","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingContentPoolItem"}},"total":{"type":"integer"}},"required":["items","total"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/content-pool/suggest":{"get":{"tags":["marketing","content-pool"],"summary":"コンテンツプールからネタ候補を返す (現状は検索と同等)","parameters":[{"schema":{"type":"string"},"required":false,"name":"keyword","in":"query"}],"responses":{"200":{"description":"候補","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingContentPoolItem"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/calendar":{"get":{"tags":["marketing","calendar"],"summary":"横断カレンダー (from-to の全チャネルイベントを日付別に返す)","description":"X / 記事 / 広告 / メール の予定・公開イベントを統合して日付別に返す。\n広告は start_date を起点として 1 日目のセルに配置 (期間中セル埋めは UI 側で展開)。\nInstagram は現状未連携のため 0 件。","parameters":[{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"}],"responses":{"200":{"description":"カレンダー","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingCalendarResponse"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/article-categories":{"get":{"tags":["marketing","article-categories"],"summary":"記事カテゴリ一覧 (is_deleted=false のみ)","parameters":[{"schema":{"type":"string"},"required":false,"name":"include_deleted","in":"query"}],"responses":{"200":{"description":"一覧","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketingArticleCategory"}}},"required":["items"]}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"post":{"tags":["marketing","article-categories"],"summary":"記事カテゴリ新規作成 (code 重複時は upsert)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"sort_order":{"type":"integer"}},"required":["code","label"]}}}},"responses":{"200":{"description":"作成済み","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingArticleCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}},"/v1/marketing/article-categories/{code}":{"get":{"tags":["marketing","article-categories"],"summary":"記事カテゴリ単体取得","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"responses":{"200":{"description":"詳細","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingArticleCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"put":{"tags":["marketing","article-categories"],"summary":"記事カテゴリを部分更新","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"更新後","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingArticleCategory"}}}}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]},"delete":{"tags":["marketing","article-categories"],"summary":"記事カテゴリを論理削除 (is_deleted=true)","parameters":[{"schema":{"type":"string"},"required":true,"name":"code","in":"path"}],"responses":{"204":{"description":"deleted"}},"x-channels":["portal-marketing"],"x-badges":[{"name":"portal-marketing","color":"#ec4899"}]}}},"webhooks":{}}