googlesheet
This commit is contained in:
377
hyundai/hyundaiCustomerVerifier.ts
Normal file
377
hyundai/hyundaiCustomerVerifier.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
// hyundaiCustomerVerifier.ts
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios";
|
||||
import qs from "qs";
|
||||
|
||||
import {
|
||||
parseCtmListTable,
|
||||
type ParsedTables,
|
||||
type ParsedRow,
|
||||
} from "./hyundaiCustomerParser";
|
||||
import type { RowObject } from "../google/index";
|
||||
|
||||
type LoginPayload = Record<string, string | number | boolean | undefined>;
|
||||
|
||||
const loginPayload = {
|
||||
app_name: "크롬(chrome)",
|
||||
from: "pc",
|
||||
nhoj: "JH1204",
|
||||
mluap: "827ccb0eea8a706c4c34a16891f84e7b",
|
||||
auth_step: 1,
|
||||
force_sms: "n",
|
||||
};
|
||||
|
||||
function clean(value?: string | null): string | null {
|
||||
const normalized = (value ?? "").replace(/\s+/g, " ").trim();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function toVerificationResult(
|
||||
row: ParsedRow
|
||||
): HyundaiCustomerVerificationResult {
|
||||
return {
|
||||
exists: true,
|
||||
receiptDate: clean(row["접수일자"]),
|
||||
customerGroup: clean(row["고객그룹"]),
|
||||
customerState: clean(row["진행상태"]),
|
||||
customerBranch: clean(row["개통처"]),
|
||||
store: clean(row["판매점"]),
|
||||
customerName: clean(row["고객명"]),
|
||||
phone: clean(row["핸드폰연락처"]),
|
||||
customerKey: clean(row["고객키"]),
|
||||
site: "",
|
||||
};
|
||||
}
|
||||
|
||||
export interface HyundaiCustomerVerifierOptions {
|
||||
/** 로그인 POST를 보낼 URL (기본값: hyun.bizmax.net 로그인 엔드포인트) */
|
||||
loginUrl?: string;
|
||||
/** 로그인 시 전송할 payload (id/pw 등) */
|
||||
loginPayload?: LoginPayload;
|
||||
|
||||
/** 폰번호 조회(또는 원하는 특정 요청)를 보낼 URL */
|
||||
requestUrl?: string;
|
||||
|
||||
/** Referer/Host 등 헤더 커스터마이즈 */
|
||||
referer?: string;
|
||||
host?: string;
|
||||
|
||||
/** 기본 User-Agent (원본 UA 유지가 유리한 서비스면 그대로 둬도 됨) */
|
||||
userAgent?: string;
|
||||
|
||||
/** 요청 타임아웃(ms) */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface HyundaiCustomerVerificationResult {
|
||||
exists?: boolean;
|
||||
receiptDate?: string | null;
|
||||
customerGroup?: string | null;
|
||||
customerState?: string | null;
|
||||
customerBranch?: string | null;
|
||||
store?: string | null;
|
||||
customerName?: string | null;
|
||||
phone: string | null;
|
||||
customerKey: string | null;
|
||||
site: string;
|
||||
}
|
||||
|
||||
export type PhoneLookupResult = HyundaiCustomerVerificationResult;
|
||||
|
||||
export class HyundaiCustomerVerifier {
|
||||
private cookie: string | null = null;
|
||||
private axios: AxiosInstance;
|
||||
private options: Required<HyundaiCustomerVerifierOptions>;
|
||||
|
||||
constructor(options: HyundaiCustomerVerifierOptions) {
|
||||
// 옵션 기본값 세팅
|
||||
this.options = {
|
||||
loginUrl: options.loginUrl ?? "https://hyun.bizmax.net/login_pcs_jr.php",
|
||||
requestUrl: options.requestUrl ?? "",
|
||||
loginPayload: loginPayload,
|
||||
referer:
|
||||
options.referer ?? "https://hyun.bizmax.net/?from=&dmy=1712135481",
|
||||
host: options.host ?? "hyun.bizmax.net",
|
||||
userAgent:
|
||||
options.userAgent ??
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36",
|
||||
timeoutMs: options.timeoutMs ?? 15_000,
|
||||
};
|
||||
|
||||
this.axios = axios.create({
|
||||
timeout: this.options.timeoutMs,
|
||||
// NOTE: 필요하면 proxy/httpsAgent 등도 여기에 설정 가능
|
||||
// httpsAgent: new https.Agent({ keepAlive: true }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 외부에서 한 번 호출해서 로그인 쿠키를 준비해줘 */
|
||||
async init(): Promise<void> {
|
||||
await this.refreshCookie();
|
||||
}
|
||||
|
||||
/** 현재 쿠키가 없거나 만료된 것으로 보이면 다시 로그인 */
|
||||
private async ensureCookie(): Promise<void> {
|
||||
if (!this.cookie) {
|
||||
await this.refreshCookie();
|
||||
}
|
||||
}
|
||||
|
||||
/** 로그인해서 Set-Cookie → cookie 문자열로 조합해 보관 */
|
||||
private async refreshCookie(): Promise<void> {
|
||||
const setCookies = await this.getCookie();
|
||||
this.cookie = this.joinSetCookie(setCookies);
|
||||
}
|
||||
|
||||
/** 실제 로그인 요청 */
|
||||
private async getCookie(): Promise<string[]> {
|
||||
const res = await this.axios.post(
|
||||
this.options.loginUrl,
|
||||
qs.stringify(this.options.loginPayload),
|
||||
{
|
||||
headers: {
|
||||
Host: this.options.host,
|
||||
Origin: "https://hyun.bizmax.net",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language":
|
||||
"ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,la;q=0.6,zh-CN;q=0.5,zh;q=0.4",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: "ds_yn=y; save_id=home3470",
|
||||
Pragma: "no-cache",
|
||||
Referer: this.options.referer,
|
||||
"User-Agent": this.options.userAgent,
|
||||
"X-BizMax-Ajax": "1",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-Site-Gubun": "pc",
|
||||
},
|
||||
// 일부 서버는 302로 세션 부여 → 따라가도 무방
|
||||
maxRedirects: 5,
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
}
|
||||
);
|
||||
|
||||
const setCookie = res.headers["set-cookie"];
|
||||
if (!setCookie || setCookie.length === 0) {
|
||||
throw new Error("로그인 실패: Set-Cookie 헤더가 없습니다.");
|
||||
}
|
||||
return setCookie;
|
||||
}
|
||||
|
||||
/** 다중 Set-Cookie 헤더를 요청용 Cookie 문자열로 합치기 */
|
||||
private joinSetCookie(setCookies: string[]): string {
|
||||
// "key=value; Path=/; HttpOnly" → "key=value"만 추출해서 세미콜론으로 연결
|
||||
const parts = setCookies
|
||||
.map((c) => c.split(";")[0]?.trim())
|
||||
.filter(Boolean);
|
||||
return parts.join("; ");
|
||||
}
|
||||
|
||||
/** 공통 요청 래퍼: 세션 만료(401/로그인 페이지 응답 등) 시 재로그인 후 재시도 */
|
||||
private async requestWithCookie<T = any>(
|
||||
config: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
await this.ensureCookie();
|
||||
|
||||
const headers = {
|
||||
Host: this.options.host,
|
||||
Referer: this.options.referer,
|
||||
"User-Agent": this.options.userAgent,
|
||||
Cookie: this.cookie!,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-Site-Gubun": "pc",
|
||||
...config.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await this.axios.request<T>({ ...config, headers });
|
||||
// 어떤 서버는 세션만료 시 200 + 로그인 HTML을 돌려주기도 함 → 가벼운 탐지
|
||||
if (
|
||||
typeof res.data === "string" &&
|
||||
/login|세션|expired|로그인/i.test(res.data)
|
||||
) {
|
||||
// 재로그인 후 1회 재시도
|
||||
await this.refreshCookie();
|
||||
const res2 = await this.axios.request<T>({
|
||||
...config,
|
||||
headers: { ...headers, Cookie: this.cookie! },
|
||||
});
|
||||
return res2.data;
|
||||
}
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
// 401 등 명시적 인증 오류 → 재로그인 후 재시도 1회
|
||||
const status = err?.response?.status;
|
||||
if (status === 401 || status === 403) {
|
||||
await this.refreshCookie();
|
||||
const res2 = await this.axios.request<T>({
|
||||
...config,
|
||||
headers: { ...headers, Cookie: this.cookie! },
|
||||
});
|
||||
return res2.data;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** 폰번호 조회 페이로드 구성(서비스에 맞게 수정) */
|
||||
private buildPhonePayload(phone: string): LoginPayload {
|
||||
// 실제 필드명/추가 파라미터는 서비스 스펙에 맞게 바꿔줘
|
||||
return { hp: phone };
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 전화번호로 현대/BizMax 고객 존재 여부를 확인합니다.
|
||||
* 현재는 조회 결과 행을 그대로 반환하고, 결과가 1개 이상이면 존재한다고 판단할 수 있습니다.
|
||||
*/
|
||||
async verifyByPhone(phone: string): Promise<any> {
|
||||
try {
|
||||
let response = await this.requestWithCookie<any>({
|
||||
url: "https://hyun.bizmax.net/customer/tpl/customer_list.php?list_type=&team_prim=&tdm_prim=&svc_gubun=0&prd_ds=&dss=&dv_prim=240",
|
||||
method: "POST",
|
||||
adapter: "fetch",
|
||||
data: qs.stringify({
|
||||
s_customer_group: "",
|
||||
s_customer_group_src: "",
|
||||
s_customer_group_dsp: "- 고객그룹",
|
||||
s_open_branch_prim: "",
|
||||
s_open_branch_prim_src: "",
|
||||
s_open_branch_prim_dsp: "- 개통처",
|
||||
s_open_id: "",
|
||||
s_sale_branch_prim: "",
|
||||
s_sale_branch_prim_src: "",
|
||||
s_sale_branch_prim_dsp: "- 판매점",
|
||||
s_sale_id: "",
|
||||
s_sale_id_2: "",
|
||||
state: "",
|
||||
state_obc: "#fff",
|
||||
state_otc: "#000",
|
||||
state_sch_ni: "",
|
||||
state_src: "",
|
||||
state_dsp: "- 진행상태",
|
||||
open_state: "",
|
||||
open_state_obc: "#fff",
|
||||
open_state_otc: "#000",
|
||||
open_state_src: "",
|
||||
open_state_dsp: "- 개통상태",
|
||||
pre_model_return: "",
|
||||
pre_model_return_obc: "#fff",
|
||||
pre_model_return_otc: "#000",
|
||||
pre_model_return_src: "",
|
||||
pre_model_return_dsp: "- 결제구분",
|
||||
excel_ext: "html",
|
||||
conf_self: "",
|
||||
conf_self_obc: "#fff",
|
||||
conf_self_otc: "#000",
|
||||
conf_self_src: "",
|
||||
conf_self_dsp: "- 2차진행상태",
|
||||
new_model_return: "",
|
||||
new_model_return_obc: "#fff",
|
||||
new_model_return_otc: "#000",
|
||||
new_model_return_src: "",
|
||||
new_model_return_dsp: "- 결제현황",
|
||||
post_size: "20",
|
||||
data_orderby: "a.reg_date",
|
||||
clm: "1",
|
||||
search_word: phone,
|
||||
sch_match: "1",
|
||||
new_model: "",
|
||||
new_model_obc: "#fff",
|
||||
new_model_otc: "#000",
|
||||
new_model_src: "",
|
||||
new_model_dsp: "- 1차 상품",
|
||||
use_date_1: "a.reg_date",
|
||||
use_date_1_src: "a.reg_date",
|
||||
use_date_1_dsp: "- 접수일자",
|
||||
start_date_1: "1970-01-01",
|
||||
end_date_1: "2026-12-31",
|
||||
use_date_2: "",
|
||||
use_date_2_src: "a.reg_date",
|
||||
use_date_2_dsp: "- 일자검색",
|
||||
start_date_2: "2025-09-04",
|
||||
end_date_2: "2025-09-04",
|
||||
age_sch_1: "1",
|
||||
age_1: "",
|
||||
age_cdn_1: "1",
|
||||
age_cdn_join: "1",
|
||||
age_sch_2: "1",
|
||||
age_2: "",
|
||||
age_cdn_2: "1",
|
||||
ac_clm: "",
|
||||
ac_sbp: "",
|
||||
sale_name_clm_q: "88bec586866ee5197e45da6a07da7c35",
|
||||
sale_name_2_clm_q: "",
|
||||
addr_fdata_clm_q: "415e2e219904aa02cce9bdf222e9bbb4",
|
||||
mode: "search",
|
||||
page: "1",
|
||||
hcv_p2: "",
|
||||
list_type: "",
|
||||
ol_type: "data",
|
||||
dv_prim: "240",
|
||||
team_prim: "",
|
||||
tdm_prim: "",
|
||||
prd_ds: "",
|
||||
fav_group_prim: "",
|
||||
no_ctm_rd_spd: "",
|
||||
no_ctm_md_spd: "",
|
||||
smr_vs: "",
|
||||
no_sch_hl: "",
|
||||
tbs_start_date: "2025-09-04",
|
||||
tbs_end_date: "2025-09-04",
|
||||
sch_addr_fdata: "",
|
||||
ctm_tbs_param: "",
|
||||
ctm_list_total_cnt: "1",
|
||||
_ihr: "n",
|
||||
_ihl: "n",
|
||||
_content_only: "y",
|
||||
site_gubun: "pc",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
Origin: "https://hyun.bizmax.net",
|
||||
Referer: "https://hyun.bizmax.net/main/?1756984315",
|
||||
"X-BizMax-Ajax": "1",
|
||||
"X-PVSN": "V1",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-Site-Gubun": "pc",
|
||||
},
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
});
|
||||
|
||||
return parseCtmListTable(String(response));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async lookupPhone(phone: string): Promise<ParsedTables> {
|
||||
return this.verifyByPhone(phone);
|
||||
}
|
||||
|
||||
async lookupPhones(rows: RowObject[]): Promise<RowObject[]> {
|
||||
const expandedRows: RowObject[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const phone = row.phone_number?.trim();
|
||||
if (!phone) {
|
||||
expandedRows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const verificationRows = await this.lookupPhone(phone);
|
||||
if (verificationRows.length === 0) {
|
||||
expandedRows.push({
|
||||
...row,
|
||||
dup_check: "중복 없음",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const verificationRow of verificationRows) {
|
||||
expandedRows.push({ ...row, ...verificationRow });
|
||||
}
|
||||
}
|
||||
|
||||
return expandedRows;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user