The scan terminal had grown into overlapping CSS, request-state, AI-provider, and city-card data responsibilities. This refactor separates those boundaries without changing product behavior: CSS modules are split by surface, city AI prompt/provider/fallback logic is isolated, and scan terminal request state now has reusable RemoteData adapters plus business-state tests. Constraint: Preserve existing global scan-terminal class names and API responses during the refactor Constraint: No new dependencies; keep this as a file-boundary cleanup Rejected: Introduce React Query now | higher migration risk than the requested lightweight query-client path Rejected: Rewrite AI stream behavior | progressive/fallback states are product-sensitive and were only adapter-split Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep AI stream state changes covered by business snapshots before changing fallback/cache wording Tested: npm run test:business; npx tsc --noEmit; npm run build; python pytest -q; ruff check; py_compile targeted city AI modules Not-tested: Live DeepSeek provider network replay and browser visual QA
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import {
|
|
buildStorageKey,
|
|
readCachedPayload,
|
|
writeCachedPayload,
|
|
} from "@/components/dashboard/scan-terminal/scan-terminal-cache";
|
|
import type { RemoteData } from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
|
import type { CityDetail, MarketScan } from "@/lib/dashboard-types";
|
|
import { normalizeCityKey } from "./decision-utils";
|
|
|
|
const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v3";
|
|
const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
|
export type CityMarketScanStatus = "idle" | "loading" | "ready" | "failed";
|
|
|
|
export type CityMarketScanSnapshot =
|
|
| { action: "reset"; cacheKey?: string }
|
|
| { action: "success"; cacheKey: string; payload: MarketScan; shouldWriteCache?: boolean }
|
|
| { action: "fetch"; cacheKey: string };
|
|
|
|
export function buildCityMarketScanCacheKey({
|
|
detailCityName,
|
|
localDate,
|
|
}: {
|
|
detailCityName: string;
|
|
localDate?: string | null;
|
|
}) {
|
|
return buildStorageKey(CITY_MARKET_SCAN_CACHE_PREFIX, [
|
|
normalizeCityKey(detailCityName),
|
|
localDate || "",
|
|
"full",
|
|
]);
|
|
}
|
|
|
|
export function readCachedCityMarketScan(cacheKey: string) {
|
|
return readCachedPayload<MarketScan>(cacheKey, CITY_MARKET_SCAN_CACHE_TTL_MS);
|
|
}
|
|
|
|
export function writeCachedCityMarketScan(cacheKey: string, payload: MarketScan) {
|
|
writeCachedPayload(cacheKey, payload);
|
|
}
|
|
|
|
export function resolveCityMarketScanSnapshot({
|
|
detail,
|
|
detailCityName,
|
|
enabled,
|
|
}: {
|
|
detail: CityDetail | null;
|
|
detailCityName: string;
|
|
enabled: boolean;
|
|
}): CityMarketScanSnapshot {
|
|
if (!detail) return { action: "reset" };
|
|
const cacheKey = buildCityMarketScanCacheKey({
|
|
detailCityName,
|
|
localDate: detail.local_date || "",
|
|
});
|
|
if (detail.market_scan) {
|
|
return {
|
|
action: "success",
|
|
cacheKey,
|
|
payload: detail.market_scan,
|
|
shouldWriteCache: true,
|
|
};
|
|
}
|
|
const cached = readCachedCityMarketScan(cacheKey);
|
|
if (cached) {
|
|
return {
|
|
action: "success",
|
|
cacheKey,
|
|
payload: cached,
|
|
};
|
|
}
|
|
return enabled ? { action: "fetch", cacheKey } : { action: "reset", cacheKey };
|
|
}
|
|
|
|
export function deriveCityMarketScanView({
|
|
detailMarketScan,
|
|
marketRemote,
|
|
}: {
|
|
detailMarketScan?: MarketScan | null;
|
|
marketRemote: RemoteData<MarketScan>;
|
|
}) {
|
|
const previousMarketScan =
|
|
marketRemote.status === "loading" || marketRemote.status === "error"
|
|
? marketRemote.previous ?? null
|
|
: null;
|
|
const marketScan =
|
|
marketRemote.status === "success"
|
|
? marketRemote.data
|
|
: previousMarketScan ?? detailMarketScan ?? null;
|
|
const marketStatus: CityMarketScanStatus =
|
|
marketRemote.status === "success"
|
|
? "ready"
|
|
: marketRemote.status === "loading"
|
|
? "loading"
|
|
: marketRemote.status === "error"
|
|
? marketScan
|
|
? "ready"
|
|
: "failed"
|
|
: "idle";
|
|
|
|
return { marketScan, marketStatus };
|
|
}
|