mmday-firebase/src/kbo/aspnet-client.ts
윤정민 c94ce69e4f Remove redundant .js extensions from import paths.
- TypeScript 소스 파일 내 모든 import 구문에서 불필요한 `.js` 확장자를 제거하여 모듈 참조 방식을 표준화했습니다.
- 핸들러, 서비스, 리포지토리 및 테스트 코드를 포함한 프로젝트 전반의 import 경로를 일관성 있게 정리했습니다.
2026-05-06 16:17:51 +09:00

179 lines
5.4 KiB
TypeScript

import { extractHiddenField } from "./html-utils";
export const PREFIX =
"ctl00$ctl00$ctl00$cphContents$cphContents$cphContents$";
export interface PageState {
viewState: string;
viewStateGenerator: string;
eventValidation: string;
/**
* ASP.NET 세션 쿠키.
*
* 일부 KBO 페이지(예: HitterBasic)는 서버가 ViewState/EventValidation을
* 세션과 연결하여 검증하므로, 초기 응답의 Set-Cookie로 받은 세션 쿠키를
* 후속 postback 요청에 포함해야 한다. 쿠키 없이 요청하면 서버가
* ViewState 검증에 실패하여 에러 페이지로 리다이렉트된다.
*
* TeamRank.aspx 같은 일부 페이지는 쿠키 없이도 동작하지만,
* 일관성을 위해 모든 요청에 쿠키를 포함한다.
*/
cookies: string;
}
export interface PostbackRequest {
url: string;
eventTarget: string;
scriptManager: string;
/**
* ASP.NET ScriptManager 컨트롤의 form field 키 이름.
*
* KBO 사이트의 ASP.NET 페이지마다 ScriptManager 컨트롤의 ID가 다르다.
* - TeamRank.aspx: PREFIX + "ScriptManager" (기본 이름)
* - HitterBasic/Basic1.aspx: PREFIX + "smData" (커스텀 이름)
*
* ScriptManager는 ASP.NET AJAX의 partial postback(UpdatePanel)을 관리하는
* 서버 컨트롤로, 요청 시 form data에 자신의 ID를 키로 포함해야 한다.
* 개발자가 페이지마다 다른 ID를 부여할 수 있으므로, 이 필드로 지정한다.
*
* 미지정 시 PREFIX + "ScriptManager"를 기본값으로 사용.
*/
scriptManagerKey?: string;
formFields: Record<string, string>;
state: PageState;
}
export interface PostbackResponse {
html: string;
newState: PageState;
}
/**
* ASP.NET AJAX delta 응답 파서.
*
* delta 응답은 `length|type|id|content|` 레코드의 연속이다.
* 기존 regex 방식(`[\s\S]*?`)은 content 내부에 `|숫자|` 패턴이 있으면
* 잘못된 위치에서 매칭이 끊기는 문제가 있었다.
* length 필드를 이용해 정확한 바이트 수만큼 content를 읽어 이 문제를 해결한다.
*/
function parseDeltaResponse(text: string): {
panels: { id: string; content: string }[];
fields: Record<string, string>;
} {
const panels: { id: string; content: string }[] = [];
const fields: Record<string, string> = {};
let pos = 0;
while (pos < text.length) {
const p1 = text.indexOf("|", pos);
if (p1 === -1) break;
const len = parseInt(text.substring(pos, p1), 10);
if (isNaN(len)) break;
pos = p1 + 1;
const p2 = text.indexOf("|", pos);
if (p2 === -1) break;
const type = text.substring(pos, p2);
pos = p2 + 1;
const p3 = text.indexOf("|", pos);
if (p3 === -1) break;
const id = text.substring(pos, p3);
pos = p3 + 1;
const content = text.substring(pos, pos + len);
pos = pos + len + 1;
if (type === "updatePanel") {
panels.push({ id, content });
} else if (type === "hiddenField") {
fields[id] = content;
}
}
return { panels, fields };
}
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/147.0.0.0 Safari/537.36";
export async function getInitialPageState(
url: string
): Promise<{ html: string; state: PageState }> {
const res = await fetch(url, {
headers: { "User-Agent": USER_AGENT },
});
const html = await res.text();
const setCookies = res.headers.getSetCookie();
const cookies = setCookies.map((c) => c.split(";")[0]).join("; ");
return {
html,
state: {
viewState: extractHiddenField(html, "__VIEWSTATE"),
viewStateGenerator: extractHiddenField(html, "__VIEWSTATEGENERATOR"),
eventValidation: extractHiddenField(html, "__EVENTVALIDATION"),
cookies,
},
};
}
export async function postback(
request: PostbackRequest
): Promise<PostbackResponse> {
const formData = new URLSearchParams();
formData.set(
request.scriptManagerKey ?? `${PREFIX}ScriptManager`,
request.scriptManager
);
for (const [key, value] of Object.entries(request.formFields)) {
formData.set(key, value);
}
formData.set("__EVENTTARGET", request.eventTarget);
formData.set("__EVENTARGUMENT", "");
formData.set("__LASTFOCUS", "");
formData.set("__VIEWSTATE", request.state.viewState);
formData.set("__VIEWSTATEGENERATOR", request.state.viewStateGenerator);
formData.set("__EVENTVALIDATION", request.state.eventValidation);
formData.set("__ASYNCPOST", "true");
const headers: Record<string, string> = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-MicrosoftAjax": "Delta=true",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": USER_AGENT,
Referer: request.url,
Origin: new URL(request.url).origin,
};
if (request.state.cookies) {
headers["Cookie"] = request.state.cookies;
}
const res = await fetch(request.url, {
method: "POST",
headers,
body: formData.toString(),
});
const text = await res.text();
const { panels, fields } = parseDeltaResponse(text);
const html = panels[0]?.content ?? text;
const newState: PageState = {
viewState: fields["__VIEWSTATE"] ?? request.state.viewState,
viewStateGenerator:
fields["__VIEWSTATEGENERATOR"] ?? request.state.viewStateGenerator,
eventValidation:
fields["__EVENTVALIDATION"] ?? request.state.eventValidation,
cookies: request.state.cookies,
};
return { html, newState };
}