diff --git a/tradein-mvp/backend/app/api/v1/admin.py b/tradein-mvp/backend/app/api/v1/admin.py index 210bcbc7..07deca99 100644 --- a/tradein-mvp/backend/app/api/v1/admin.py +++ b/tradein-mvp/backend/app/api/v1/admin.py @@ -931,3 +931,92 @@ async def scrape_yandex_valuation( total_objects=result.house.total_objects, history_items_count=len(result.history_items), ) + + +class CianDetailTriggerResp(BaseModel): + ok: bool + offer_url: str + cian_id: int | None + price_changes_count: int + saved: bool + listing_id: int | None + + +@router.post("/scrape/cian-detail", response_model=CianDetailTriggerResp) +async def scrape_cian_detail( + offer_url: str, + listing_id: int | None = None, + db: Session = Depends(get_db), # noqa: B008 +) -> CianDetailTriggerResp: + """Ad-hoc parse one Cian offer detail page. + + If `listing_id` provided → save enrichment (offer_price_history + listings updates). + Without it → debug-only (no DB write). + """ + from app.services.scrapers.cian_detail import fetch_detail, save_detail_enrichment + + enrichment = await fetch_detail(offer_url) + if enrichment is None: + raise HTTPException(404, f"Could not parse Cian detail page: {offer_url}") + + saved = False + if listing_id is not None: + save_detail_enrichment(db, listing_id, enrichment) + saved = True + + return CianDetailTriggerResp( + ok=True, + offer_url=offer_url, + cian_id=enrichment.cian_id, + price_changes_count=len(enrichment.price_changes), + saved=saved, + listing_id=listing_id, + ) + + +class CianNewbuildingTriggerResp(BaseModel): + ok: bool + zhk_url: str + cian_internal_house_id: int | None + name: str | None + price_dynamics_count: int + has_management_company: bool + saved: bool + house_id: int | None + + +@router.post("/scrape/cian-newbuilding", response_model=CianNewbuildingTriggerResp) +async def scrape_cian_newbuilding( + zhk_url: str, + house_id: int | None = None, + db: Session = Depends(get_db), # noqa: B008 +) -> CianNewbuildingTriggerResp: + """Ad-hoc parse a Cian ЖК (newbuilding) catalog page. + + If `house_id` provided → persist (houses + houses_price_dynamics + management_companies). + Without it → debug-only (no DB write). + """ + from app.services.scrapers.cian_newbuilding import ( + fetch_newbuilding, + save_newbuilding_enrichment, + ) + + enrichment = await fetch_newbuilding(zhk_url) + if enrichment is None: + raise HTTPException(404, f"Could not parse Cian newbuilding page: {zhk_url}") + + saved = False + if house_id is not None: + await save_newbuilding_enrichment(db, house_id, enrichment) + saved = True + + return CianNewbuildingTriggerResp( + ok=True, + zhk_url=zhk_url, + cian_internal_house_id=enrichment.cian_internal_house_id, + name=enrichment.name, + price_dynamics_count=len(enrichment.realty_valuation_chart), + has_management_company=bool(enrichment.management_company), + saved=saved, + house_id=house_id, + ) diff --git a/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx b/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx index 1374d8f8..835b6818 100644 --- a/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx +++ b/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx @@ -28,6 +28,26 @@ interface TestAuthResp { reason: string | null; } +interface CianDetailTriggerResp { + ok: boolean; + offer_url: string; + cian_id: number | null; + price_changes_count: number; + saved: boolean; + listing_id: number | null; +} + +interface CianNewbuildingTriggerResp { + ok: boolean; + zhk_url: string; + cian_internal_house_id: number | null; + name: string | null; + price_dynamics_count: number; + has_management_company: boolean; + saved: boolean; + house_id: number | null; +} + interface ScraperSetting { source: string; request_delay_sec: number; @@ -91,6 +111,40 @@ function useUpdateScraperSetting() { }); } +function useScrapeCianDetail() { + return useMutation< + CianDetailTriggerResp, + Error, + { offer_url: string; listing_id: number | undefined } + >({ + mutationFn: ({ offer_url, listing_id }) => { + const qs = new URLSearchParams({ offer_url }); + if (listing_id !== undefined) qs.set("listing_id", String(listing_id)); + return apiFetch( + `/api/v1/admin/scrape/cian-detail?${qs.toString()}`, + { method: "POST" }, + ); + }, + }); +} + +function useScrapeCianNewbuilding() { + return useMutation< + CianNewbuildingTriggerResp, + Error, + { zhk_url: string; house_id: number | undefined } + >({ + mutationFn: ({ zhk_url, house_id }) => { + const qs = new URLSearchParams({ zhk_url }); + if (house_id !== undefined) qs.set("house_id", String(house_id)); + return apiFetch( + `/api/v1/admin/scrape/cian-newbuilding?${qs.toString()}`, + { method: "POST" }, + ); + }, + }); +} + // ── Result panel ─────────────────────────────────────────────────────────── interface ResultPanelProps { @@ -133,6 +187,16 @@ export default function CianScraperPage() { const [multiRoom, setMultiRoom] = useState(false); const aroundMut = useScrapeAround(); + // 2. Detail by URL + const [detailUrl, setDetailUrl] = useState(""); + const [detailListingId, setDetailListingId] = useState(""); + const detailMut = useScrapeCianDetail(); + + // 3. Newbuilding + const [nbZhkUrl, setNbZhkUrl] = useState(""); + const [nbHouseId, setNbHouseId] = useState(""); + const nbMut = useScrapeCianNewbuilding(); + // Global delay const settingsQ = useScraperSettings(); const updateSettingMut = useUpdateScraperSetting(); @@ -255,55 +319,100 @@ export default function CianScraperPage() { - {/* 2. Scrape detail by URL — NOT YET IMPLEMENTED BACKEND */} + {/* 2. Detail by URL */}

2. Detail page enrichment

- Парсинг конкретного объявления Cian по URL. Endpoint{" "} - POST /api/v1/admin/scrape/cian-detail планируется в - следующем PR. Пока используйте поиск по anchor (#1) для массового - скрейпа. + Парсинг конкретного объявления Cian по URL: цена, история цен, сохранение + в cian_price_dynamics. Если listing_id не указан — только + debug-режим без записи в БД.

-
{ + e.preventDefault(); + detailMut.mutate({ + offer_url: detailUrl, + listing_id: detailListingId !== "" ? parseInt(detailListingId, 10) : undefined, + }); + }} > - Endpoint не реализован на backend. Запрос вернёт 404. -
+ + + + +
- {/* 3. Newbuilding — NOT YET IMPLEMENTED BACKEND */} + {/* 3. Newbuilding */}

3. Новостройки (ЖК scrape)

- Endpoint POST /api/v1/admin/scrape/cian-newbuilding{" "} - планируется. Для Yandex Недвижимость аналог уже доступен на вкладке - Yandex. + Скрейп landing-страницы ЖК Cian: динамика цен, управляющая компания. + Если house_id не указан — только debug-режим без записи в БД.

-
{ + e.preventDefault(); + nbMut.mutate({ + zhk_url: nbZhkUrl, + house_id: nbHouseId !== "" ? parseInt(nbHouseId, 10) : undefined, + }); + }} > - Endpoint не реализован на backend. Запрос вернёт 404. -
+ + + + +
- {/* 4. Valuation Calculator — NOT YET IMPLEMENTED BACKEND */} + {/* 4. Valuation Calculator */}

4. Valuation Calculator (оценка квартиры)

- Endpoint POST /api/v1/admin/scrape/cian-valuation{" "} - планируется. Требует загруженных cookies (Stage 7).{" "} - - Настроить cookies → - + Calculator вызывается автоматически из /estimate flow (Stage 9). + Отдельный admin trigger будет добавлен позже.

-
- Endpoint не реализован на backend. Запрос вернёт 404. -
{/* 5. Global Request Delay */}