feat(tradein-ui): wire /scrapers/cian sections 2+3 to new backend endpoints

This commit is contained in:
lekss361 2026-05-24 16:54:27 +03:00
parent 9234cae932
commit 2d7446a4a9

View file

@ -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: encodeURIComponent(offer_url) });
if (listing_id !== undefined) qs.set("listing_id", String(listing_id));
return apiFetch<CianDetailTriggerResp>(
`/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: encodeURIComponent(zhk_url) });
if (house_id !== undefined) qs.set("house_id", String(house_id));
return apiFetch<CianNewbuildingTriggerResp>(
`/api/v1/admin/scrape/cian-newbuilding?${qs.toString()}`,
{ method: "POST" },
);
},
});
}
// ── Result panel ───────────────────────────────────────────────────────────
interface ResultPanelProps<TData> {
@ -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() {
<ResultPanel mut={aroundMut} />
</section>
{/* 2. Scrape detail by URL — NOT YET IMPLEMENTED BACKEND */}
{/* 2. Detail by URL */}
<section className="scraper-section">
<h2>2. Detail page enrichment</h2>
<p className="scraper-hint">
Парсинг конкретного объявления Cian по URL. Endpoint{" "}
<code>POST /api/v1/admin/scrape/cian-detail</code> планируется в
следующем PR. Пока используйте поиск по anchor (#1) для массового
скрейпа.
Парсинг конкретного объявления Cian по URL: цена, история цен, сохранение
в cian_price_dynamics. Если <code>listing_id</code> не указан только
debug-режим без записи в БД.
</p>
<div
className="scraper-result scraper-result--error"
style={{ marginTop: 0 }}
<form
className="scraper-form"
onSubmit={(e) => {
e.preventDefault();
detailMut.mutate({
offer_url: detailUrl,
listing_id: detailListingId !== "" ? parseInt(detailListingId, 10) : undefined,
});
}}
>
Endpoint не реализован на backend. Запрос вернёт 404.
</div>
<label className="full">
offer_url
<input
type="url"
value={detailUrl}
onChange={(e) => setDetailUrl(e.target.value)}
required
placeholder="https://ekb.cian.ru/sale/flat/XXXXX/"
/>
</label>
<label>
listing_id (опционально)
<input
type="number"
value={detailListingId}
onChange={(e) => setDetailListingId(e.target.value)}
placeholder="оставьте пустым для debug-режима"
/>
</label>
<button type="submit" disabled={detailMut.isPending}>
{detailMut.isPending ? "Парсим…" : "Запустить"}
</button>
</form>
<ResultPanel mut={detailMut} />
</section>
{/* 3. Newbuilding — NOT YET IMPLEMENTED BACKEND */}
{/* 3. Newbuilding */}
<section className="scraper-section">
<h2>3. Новостройки (ЖК scrape)</h2>
<p className="scraper-hint">
Endpoint <code>POST /api/v1/admin/scrape/cian-newbuilding</code>{" "}
планируется. Для Yandex Недвижимость аналог уже доступен на вкладке
Yandex.
Скрейп landing-страницы ЖК Cian: динамика цен, управляющая компания.
Если <code>house_id</code> не указан только debug-режим без записи в БД.
</p>
<div
className="scraper-result scraper-result--error"
style={{ marginTop: 0 }}
<form
className="scraper-form"
onSubmit={(e) => {
e.preventDefault();
nbMut.mutate({
zhk_url: nbZhkUrl,
house_id: nbHouseId !== "" ? parseInt(nbHouseId, 10) : undefined,
});
}}
>
Endpoint не реализован на backend. Запрос вернёт 404.
</div>
<label className="full">
zhk_url
<input
type="url"
value={nbZhkUrl}
onChange={(e) => setNbZhkUrl(e.target.value)}
required
placeholder="https://zhk-XXX-ekb-i.cian.ru/"
/>
</label>
<label>
house_id (опционально)
<input
type="number"
value={nbHouseId}
onChange={(e) => setNbHouseId(e.target.value)}
placeholder="оставьте пустым для debug-режима"
/>
</label>
<button type="submit" disabled={nbMut.isPending}>
{nbMut.isPending ? "Парсим…" : "Запустить"}
</button>
</form>
<ResultPanel mut={nbMut} />
</section>
{/* 4. Valuation Calculator — NOT YET IMPLEMENTED BACKEND */}
{/* 4. Valuation Calculator */}
<section className="scraper-section">
<h2>4. Valuation Calculator (оценка квартиры)</h2>
<p className="scraper-hint">
Endpoint <code>POST /api/v1/admin/scrape/cian-valuation</code>{" "}
планируется. Требует загруженных cookies (Stage 7).{" "}
<Link href="/scrapers/cian-cookies">
Настроить cookies
</Link>
Calculator вызывается автоматически из /estimate flow (Stage 9).
Отдельный admin trigger будет добавлен позже.
</p>
<div
className="scraper-result scraper-result--error"
style={{ marginTop: 0 }}
>
Endpoint не реализован на backend. Запрос вернёт 404.
</div>
</section>
{/* 5. Global Request Delay */}