]*>([\s\S]*?)<\/tr>/gi;
+ let trMatch: RegExpExecArray | null;
+
+ while ((trMatch = trRegex.exec(html)) !== null) {
+ const trContent = trMatch[1];
+ const tds: string[] = [];
+ const tdRegex = /| ]*>([\s\S]*?)<\/td>/gi;
+ let tdMatch: RegExpExecArray | null;
+ while ((tdMatch = tdRegex.exec(trContent)) !== null) {
+ tds.push(stripTags(decodeHtmlEntities(tdMatch[1])));
+ }
+
+ // 타자 기록 행: 16컬럼 (순위, 선수명, 팀, AVG, G, PA, AB, R, H, 2B, 3B, HR, TB, RBI, SAC, SF)
+ if (tds.length >= 16) {
+ const rank = tds[0];
+ if (/^\d+$/.test(rank)) {
+ hitters.push({
+ rank,
+ player: tds[1],
+ team: tds[2],
+ avg: tds[3],
+ games: tds[4],
+ pa: tds[5],
+ ab: tds[6],
+ runs: tds[7],
+ hits: tds[8],
+ doubles: tds[9],
+ triples: tds[10],
+ hr: tds[11],
+ tb: tds[12],
+ rbi: tds[13],
+ sac: tds[14],
+ sf: tds[15],
+ });
+ }
+ }
+ }
+
+ return hitters;
+}
+
+// ── Form fields ──
+
+function buildFormFields(
+ year: number,
+ teamCode: string
+): Record {
+ return {
+ [`${PREFIX}ddlSeason$ddlSeason`]: String(year),
+ [`${PREFIX}ddlSeries$ddlSeries`]: "0",
+ [`${PREFIX}ddlTeam$ddlTeam`]: teamCode,
+ [`${PREFIX}ddlPos$ddlPos`]: "",
+ [`${PREFIX}ddlSituation$ddlSituation`]: "",
+ [`${PREFIX}ddlSituationDetail$ddlSituationDetail`]: "",
+ [`${PREFIX}hfOrderByCol`]: "HRA_RT",
+ [`${PREFIX}hfOrderBy`]: "DESC",
+ [`${PREFIX}hfPage`]: "1",
+ };
+}
+
+// ── Fetch ──
+
+export async function fetchHitterStatsInitial(): Promise<{
+ result: HitterStatsResult;
+ state: PageState;
+}> {
+ const { html, state } = await getInitialPageState(PAGE_URL);
+
+ const yearMatch = html.match(
+ /ddlSeason_ddlSeason[\s\S]*?selected="selected"\s+value="(\d{4})"/
+ );
+ const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
+
+ return {
+ result: { year, team: "", hitters: parseHitterTable(html) },
+ state,
+ };
+}
+
+export async function fetchHitterStats(
+ year: number,
+ state: PageState,
+ teamCode = ""
+): Promise<{ result: HitterStatsResult; newState: PageState }> {
+ const eventTarget = `${PREFIX}ddlSeason$ddlSeason`;
+
+ const { html, newState }: PostbackResponse = await postback({
+ url: PAGE_URL,
+ eventTarget,
+ scriptManagerKey: SM_KEY,
+ scriptManager: `${UPDATE_PANEL}|${eventTarget}`,
+ formFields: buildFormFields(year, teamCode),
+ state,
+ });
+
+ // 팀 필터가 있으면 추가 postback
+ if (teamCode) {
+ const teamTarget = `${PREFIX}ddlTeam$ddlTeam`;
+ const { html: teamHtml, newState: teamState } = await postback({
+ url: PAGE_URL,
+ eventTarget: teamTarget,
+ scriptManagerKey: SM_KEY,
+ scriptManager: `${UPDATE_PANEL}|${teamTarget}`,
+ formFields: buildFormFields(year, teamCode),
+ state: newState,
+ });
+
+ return {
+ result: { year, team: teamCode, hitters: parseHitterTable(teamHtml) },
+ newState: teamState,
+ };
+ }
+
+ return {
+ result: { year, team: "", hitters: parseHitterTable(html) },
+ newState,
+ };
+}
+
+/**
+ * 모든 페이지의 타자 기록을 가져온다.
+ * 초기 페이지 로드 후, 페이지네이션 postback을 반복하여 전체 데이터를 수집한다.
+ */
+export async function fetchAllHitterStats(
+ year: number,
+ state: PageState,
+ teamCode = ""
+): Promise<{ result: HitterStatsResult; newState: PageState }> {
+ // 첫 페이지 가져오기
+ const { result: firstResult, newState: firstState } = await fetchHitterStats(
+ year,
+ state,
+ teamCode
+ );
+
+ const allHitters = [...firstResult.hitters];
+ let currentState = firstState;
+ let page = 2;
+
+ // 다음 페이지가 있는 동안 반복
+ while (true) {
+ const pagerTarget = `${PREFIX}ucPager$btnNo${page}`;
+ const fields = buildFormFields(year, teamCode);
+ fields[`${PREFIX}hfPage`] = String(page);
+
+ const { html, newState }: PostbackResponse = await postback({
+ url: PAGE_URL,
+ eventTarget: pagerTarget,
+ scriptManagerKey: SM_KEY,
+ scriptManager: `${UPDATE_PANEL}|${pagerTarget}`,
+ formFields: fields,
+ state: currentState,
+ });
+
+ const pageHitters = parseHitterTable(html);
+ if (pageHitters.length === 0) break;
+
+ allHitters.push(...pageHitters);
+ currentState = newState;
+ page++;
+ }
+
+ return {
+ result: { year, team: teamCode, hitters: allHitters },
+ newState: currentState,
+ };
+}
\ No newline at end of file
|