첫번째 완료

This commit is contained in:
kjy
2025-11-28 08:44:20 +09:00
parent 25ad517d01
commit 5c57f1c582
14 changed files with 2859 additions and 177 deletions

View File

@@ -24,6 +24,69 @@ axiosRetry(axiosInstance, {
});
export class NaverRealEstate {
async getRankingToken() {
try {
const response = await axios.get("https://new.land.naver.com/houses", {
proxy: {
host: "gw.dataimpulse.com",
port: 823,
auth: {
username: "0bdeb90b7713c370cdeb__cr.kr",
password: "a5ae50d6913bd778",
},
},
params: {
ms: "37.532448,127.0014059,19",
a: "VL",
b: "B2",
e: "RETAIL",
},
headers: {
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "ko;q=0.8",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "u=0, i",
"sec-ch-ua":
'"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"sec-gpc": "1",
"upgrade-insecure-requests": "1",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
},
});
const html = response.data;
// HTML에서 rankingToken 추출
const tokenMatch = html.match(/token["'\s:]+["']([^"']+)["']/);
const rankingToken = tokenMatch ? tokenMatch[1] : null;
if (!rankingToken) {
throw new Error("rankingToken을 찾을 수 없습니다");
}
// 쿠키 추출
const cookies = response.headers["set-cookie"];
const cookieString = cookies
? cookies.map((cookie: string) => cookie.split(";")[0]).join("; ")
: "";
return {
token: rankingToken,
cookie: cookieString,
};
} catch (error) {
console.error(`❌ 오류 발생:`, error);
throw error;
}
}
private realtorId: string;
private seed: string | null;
private client: AxiosInstance;
@@ -67,44 +130,78 @@ export class NaverRealEstate {
/**
* Seed 값을 가져옵니다 (DB에 저장)
*/
async fetchSeed(): Promise<string> {
async fetchSeed(cookie: string): Promise<string> {
try {
const response = await axios.get(
`https://fin.land.naver.com/realtor/${this.realtorId}`,
{
headers: {
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "ko;q=0.7",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "u=0, i",
referer: "https://m.land.naver.com/",
"sec-ch-ua":
'"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": '"Android"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"sec-gpc": "1",
"upgrade-insecure-requests": "1",
"user-agent":
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36",
let data = JSON.stringify({
size: 30,
realtorId: "namyeong00",
userChannelType: "PC",
tradeTypes: ["A1", "B1", "B2", "B3"],
realestateTypes: [
"A01",
"A02",
"A06",
"C02",
"C03",
"C04",
"D01",
"D02",
"D03",
"D04",
"D05",
"E01",
"E03",
],
articleSortType: "RANKING_DESC",
lastInfo: [],
});
let config = {
method: "post",
maxBodyLength: Infinity,
url: "https://fin.land.naver.com/front-api/v1/realtor/articles",
proxy: {
host: "gw.dataimpulse.com",
port: 823,
auth: {
username: "0bdeb90b7713c370cdeb__cr.kr",
password: "a5ae50d6913bd778",
},
}
);
},
headers: {
connection: "close",
accept: "application/json, text/plain, */*",
"accept-language": "ko;q=0.7",
"cache-control": "no-cache",
"content-type": "application/json",
origin: "https://fin.land.naver.com",
pragma: "no-cache",
priority: "u=1, i",
referer: "https://fin.land.naver.com/realtor/namyeong00",
"sec-ch-ua":
'"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"sec-gpc": "1",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
cookie: cookie,
},
data: data,
};
// HTML 응답에서 seed 값 추출
const html = response.data;
const seedMatch = html.match(/"seed":"([a-f0-9-]+)"/);
const response = await axios.request(config);
if (!seedMatch || !seedMatch[1]) {
// body 응답에서 seed 값 추출
const seed = response.data?.result?.seed;
if (!seed) {
throw new Error("Seed 값을 찾을 수 없습니다");
}
const extractedSeed = seedMatch[1];
const extractedSeed = seed;
this.seed = extractedSeed;
return extractedSeed;
@@ -902,80 +999,55 @@ export class NaverRealEstate {
}
}
async getRanking(article: RealEstateArticle): Promise<number> {
async getRanking(
article: RealEstateArticle,
token: string,
cookie: string
): Promise<number> {
try {
const lft = article.xCoordinate || 0 + 0.000283244;
const rgt = article.xCoordinate || 0 - 0.000283244;
const top = article.yCoordinate || 0 + 0.000283244;
const btm = article.yCoordinate || 0 - 0.000283244;
// const url = `https://m.land.naver.com/cluster/ajax/articleList?itemId=${article.lgeo}&mapKey=&lgeo=${article.lgeo}&rletTpCd=APT:OPST:VL:YR:DSD:ABYG:OBYG:JGC:JWJT:DDDGG:SGJT:JGB:OR:SG:SMS:GJCG:GM:TJ:APTHGJ&tradTpCd=A1:B1:B2:B3&z=19&lat=${article.yCoordinate}&lon=${article.xCoordinate}&btm=${btm}&lft=${lft}&top=${top}&rgt=${rgt}&cortarNo=&showR0=`;
const url = `https://new.land.naver.com/api/articles?markerId=${article.lgeo}&markerType=LGEOHASH_MIX_ARTICLE&prevScrollTop=0&order=rank&realEstateType=${article.realEstateType}&tradeType=${article.tradeType}&rentPriceMin=0&rentPriceMax=900000000&priceMin=0&priceMax=900000000&areaMin=0&areaMax=900000000&oldBuildYears&recentlyBuildYears&minHouseHoldCount&maxHouseHoldCount&showArticle=false&sameAddressGroup=false&minMaintenanceCost&maxMaintenanceCost&priceType=RETAIL&directions=&page=1&articleState`;
// const response = await axiosInstance.get(url, {
// proxy: {
// host: "gw.dataimpulse.com",
// port: 823,
// auth: {
// username: "0bdeb90b7713c370cdeb__cr.kr",
// password: "a5ae50d6913bd778",
// },
// },
// headers: {
// accept: "application/json, text/javascript, */*; q=0.01",
// "accept-language": "ko;q=0.7",
// "cache-control": "no-cache",
// pragma: "no-cache",
// priority: "u=0, i",
// referer: "https://m.land.naver.com/",
// "sec-ch-ua":
// '"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
// "sec-ch-ua-mobile": "?1",
// "sec-ch-ua-platform": '"Android"',
// "sec-fetch-dest": "empty",
// "sec-fetch-mode": "cors",
// "sec-fetch-site": "same-origin",
// "sec-gpc": "1",
// "user-agent":
// "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36",
// "x-requested-with": "XMLHttpRequest",
// },
// });
// const url = `https://new.land.naver.com/api/articles?markerId=${article.lgeo}&markerType=LGEOHASH_MIX_ARTICLE&prevScrollTop=0&order=rank&realEstateType=${article.realEstateType}&tradeType=${article.tradeType}&rentPriceMin=0&rentPriceMax=900000000&priceMin=0&priceMax=900000000&areaMin=0&areaMax=900000000&oldBuildYears&recentlyBuildYears&minHouseHoldCount&maxHouseHoldCount&showArticle=false&sameAddressGroup=false&minMaintenanceCost&maxMaintenanceCost&priceType=RETAIL&directions=&page=1&articleState`;
// const response = await axiosInstance.get(url, {
// proxy: {
// host: "gw.dataimpulse.com",
// port: 823,
// auth: {
// username: "0bdeb90b7713c370cdeb__cr.kr",
// password: "a5ae50d6913bd778",
// },
// },
// headers: {
// accept: "*/*",
// "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",
// pragma: "no-cache",
// priority: "u=1, i",
// referer: `https://new.land.naver.com/offices?ms=37.5650651,126.9309659,18&a=${article.realEstateType}&b=${article.tradeType}&e=RETAIL`,
// "sec-ch-ua":
// '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
// "sec-ch-ua-mobile": "?0",
// "sec-ch-ua-platform": '"Windows"',
// "sec-fetch-dest": "empty",
// "sec-fetch-mode": "cors",
// "sec-fetch-site": "same-origin",
// "user-agent":
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
// "Content-Type": "text/plain",
// },
// });
const response = await axiosInstance.get(url, {
proxy: {
host: "gw.dataimpulse.com",
port: 823,
auth: {
username: "0bdeb90b7713c370cdeb__cr.kr",
password: "a5ae50d6913bd778",
},
},
headers: {
accept: "*/*",
"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",
pragma: "no-cache",
priority: "u=1, i",
referer: `https://new.land.naver.com/offices?ms=37.5650651,126.9309659,18&a=${article.realEstateType}&b=${article.tradeType}&e=RETAIL`,
"sec-ch-ua":
'"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
"Content-Type": "text/plain",
Authorization: `Bearer ${token}`,
Cookie: cookie,
},
});
// body 배열에서 articleNumber와 일치하는 atclNo 찾기
const body = response.data.body || [];
const index = body.findIndex(
(item: any) => item.atclNo === article.articleNumber
const rankingList = response.data.articleList || [];
const index = rankingList.findIndex(
(item: any) => item.articleNo === article.articleNumber
);
// index가 -1이면 찾지 못한 것이므로 99 반환, 아니면 index + 1 반환

View File

@@ -1,6 +1,7 @@
import type { RealEstateArticle } from "../generated/prisma";
import type { NaverRealEstate } from "./naver.service";
import prisma from "../lib/prisma";
import pLimit from "p-limit";
export class RankingService {
private naver: NaverRealEstate;
@@ -8,32 +9,43 @@ export class RankingService {
this.naver = naver;
}
async updateRanking(articles: RealEstateArticle[], checkDate: Date) {
try {
for (let article of articles) {
// cortarNo, lgeo 확인
if (!article.cortarNo || !article.lgeo) {
const { cortarNo, lgeo } = await this.updateCortarNoAndLgeo(article);
article.cortarNo = cortarNo;
article.lgeo = lgeo;
const { token, cookie } = await this.naver.getRankingToken();
const limit = pLimit(20);
const tasks = articles.map((article) => {
return limit(async () => {
try {
// cortarNo, lgeo 확인
if (!article.cortarNo || !article.lgeo) {
const { cortarNo, lgeo } = await this.updateCortarNoAndLgeo(
article
);
article.cortarNo = cortarNo;
article.lgeo = lgeo;
}
// 랭킹 조회 및 업데이트
const ranking = await this.naver.getRanking(article, token, cookie);
await prisma.realEstateArticle.update({
where: { id: article.id },
data: {
ranking,
rankCheckDate: checkDate.toISOString(),
},
});
console.log(
`${article.articleNumber} - 랭킹 업데이트 완료: ${ranking}`
);
} catch (error) {
console.error(`❌ updateRanking 오류:`, error);
}
// 랭킹 조회 및 업데이트
const ranking = await this.naver.getRanking(article);
await prisma.realEstateArticle.update({
where: { id: article.id },
data: {
ranking,
rankCheckDate: checkDate.toISOString(),
},
});
console.log(
`${article.articleNumber} - 랭킹 업데이트 완료: ${ranking}`
);
}
} catch (error) {
console.error(`❌ updateRanking 오류:`, error);
}
});
});
console.log("🔹 예약된 작업 수:", tasks.length);
const results = await Promise.allSettled(tasks);
console.log("🟢 모든 limit 작업 완료:", results.length);
await Bun.sleep(100);
}
async updateCortarNoAndLgeo(article: RealEstateArticle): Promise<{

View File

@@ -1,4 +1,5 @@
import TelegramBot from "node-telegram-bot-api";
import fs from "fs";
export interface TelegramUser {
site: string;
@@ -6,6 +7,7 @@ export interface TelegramUser {
name: string;
phone: string;
realtorId: string;
brokerPhone?: string;
}
export const telegramUsers: TelegramUser[] = [
@@ -57,6 +59,7 @@ export const telegramUsers: TelegramUser[] = [
name: "국성혜",
phone: "010-8305-1291",
realtorId: "namyeong00",
brokerPhone: "010-8305-1291",
},
{
site: "부동산포스",
@@ -64,6 +67,7 @@ export const telegramUsers: TelegramUser[] = [
name: "이경희",
phone: "010-6346-0996",
realtorId: "namyeong00",
brokerPhone: "010-5947-0996",
},
{
site: "산업부동산",
@@ -133,7 +137,7 @@ export class TelegramService {
try {
await this.bot.sendDocument(
chatId,
fileName,
fs.createReadStream(fileName),
{
caption: caption || "",
},