첫번째 완료
This commit is contained in:
178
src/app.ts
Normal file
178
src/app.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import prisma from "./lib/prisma";
|
||||
import dayjs from "dayjs";
|
||||
import { RankingService } from "./services/ranking.service";
|
||||
import { NaverRealEstate } from "./services/naver.service";
|
||||
import { realestateTypes, realtorIds } from "./config";
|
||||
|
||||
import {
|
||||
TelegramService,
|
||||
telegramUsers,
|
||||
testUsers,
|
||||
type TelegramUser,
|
||||
} from "./services/telegram.service";
|
||||
import type { RealEstateArticle } from "./generated/prisma";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
const isTestMode = process.argv.includes("--test");
|
||||
|
||||
const telegramService = new TelegramService(
|
||||
"233460568:AAHWgRQo5IgcWR0uXdsiMEzNnsmIqjOgk24",
|
||||
false
|
||||
);
|
||||
|
||||
async function main() {
|
||||
console.time("updateRanking");
|
||||
await updateRanking();
|
||||
console.timeEnd("updateRanking");
|
||||
console.time("sendTelegram");
|
||||
await sendTelegram();
|
||||
console.timeEnd("sendTelegram");
|
||||
}
|
||||
|
||||
async function sendTelegram() {
|
||||
const sendUsers = isTestMode ? testUsers : telegramUsers;
|
||||
|
||||
for (let telegramUser of sendUsers) {
|
||||
const articles = await prisma.realEstateArticle.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
realtorId: telegramUser.realtorId,
|
||||
...(telegramUser.site !== "ALL" && { cpNm: telegramUser.site }),
|
||||
...(telegramUser.site === "부동산포스" && {
|
||||
brokerPhone: telegramUser.brokerPhone,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (articles.length === 0) {
|
||||
console.log("no articles");
|
||||
continue;
|
||||
}
|
||||
|
||||
const excelFilePath = await createExcelFile(telegramUser, articles);
|
||||
|
||||
await telegramService.sendDocument(
|
||||
telegramUser.chatId,
|
||||
excelFilePath,
|
||||
`네이버 부동산 매물 목록 (${dayjs().format("YYYY-MM-DD")})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createExcelFile(
|
||||
telegramUser: TelegramUser,
|
||||
articles: RealEstateArticle[]
|
||||
): Promise<string> {
|
||||
// 데이터를 배열로 변환
|
||||
const data = articles.map((item) => {
|
||||
const getOwnerType = () => {
|
||||
if (["MOBL", "NDOC1", "OWNER"].includes(item.verificationType || "")) {
|
||||
if (
|
||||
["VL", "APT", "OPST", "DDDGG"].includes(item.realEstateType || "")
|
||||
) {
|
||||
return "집주인";
|
||||
} else {
|
||||
return "소유자";
|
||||
}
|
||||
}
|
||||
if (["SITE", "S_VR"].includes(item.verificationType || "")) {
|
||||
return "현장";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getPrice = () => {
|
||||
return item.prcInfo;
|
||||
};
|
||||
|
||||
return {
|
||||
매물번호: item.articleNumber,
|
||||
바로가기: `https://fin.land.naver.com/articles/${item.articleNumber}`,
|
||||
소유자구분: getOwnerType(),
|
||||
매물형태: item.articleName || "",
|
||||
매매구분: item.tradTpNm || "",
|
||||
주소: `${item.city || ""} ${item.division || ""} ${item.sector || ""}`,
|
||||
상세주소: item.detailAddress || "",
|
||||
층수: item.floorInfo || "",
|
||||
가격: getPrice(),
|
||||
매물특징: item.articleDescription || "",
|
||||
광고사: item.cpNm || "",
|
||||
중계사전화번호: item.brokerPhone || "",
|
||||
확인일자: item.articleConfirmDate
|
||||
? dayjs(item.articleConfirmDate).format("YYYY-MM-DD")
|
||||
: "",
|
||||
순위: item.ranking || 9999,
|
||||
};
|
||||
});
|
||||
|
||||
// 워크시트 생성
|
||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||
|
||||
// 바로가기 컬럼에 하이퍼링크 추가 (B열, 2번째 컬럼)
|
||||
articles.forEach((item, index) => {
|
||||
const cellAddress = `B${index + 2}`; // 헤더가 1행이므로 데이터는 2행부터
|
||||
const url = `https://fin.land.naver.com/articles/${item.articleNumber}`;
|
||||
worksheet[cellAddress] = {
|
||||
t: "s", // string type
|
||||
v: "열기", // 표시되는 텍스트
|
||||
l: { Target: url }, // hyperlink
|
||||
};
|
||||
});
|
||||
|
||||
// 컬럼 너비 설정
|
||||
worksheet["!cols"] = [
|
||||
{ wch: 12 }, // 매물번호
|
||||
{ wch: 10 }, // 바로가기
|
||||
{ wch: 10 }, // 소유자구분
|
||||
{ wch: 12 }, // 매물형태
|
||||
{ wch: 10 }, // 매매구분
|
||||
{ wch: 30 }, // 주소
|
||||
{ wch: 40 }, // 상세주소
|
||||
{ wch: 8 }, // 층수
|
||||
{ wch: 14 }, // 가격
|
||||
{ wch: 30 }, // 매물특징
|
||||
{ wch: 14 }, // 광고사
|
||||
{ wch: 14 }, // 중계사전화번호
|
||||
{ wch: 10 }, // 확인일자
|
||||
{ wch: 6 }, // 순위
|
||||
];
|
||||
|
||||
// 워크북 생성
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "매물목록");
|
||||
|
||||
// 파일 저장
|
||||
const filePath = `./xlsx/${dayjs().format("YYYY-MM-DD_HH-mm")}_순위_${
|
||||
telegramUser.site
|
||||
}_${crypto.randomUUID().slice(0, 8)}.xlsx`;
|
||||
|
||||
XLSX.writeFile(workbook, filePath);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function updateRanking() {
|
||||
try {
|
||||
const checkDate = dayjs().toDate();
|
||||
for (let realtorId of realtorIds) {
|
||||
const articles = await prisma.realEstateArticle.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
realtorId: realtorId,
|
||||
},
|
||||
});
|
||||
|
||||
const rankingService = new RankingService(
|
||||
new NaverRealEstate({
|
||||
realtorId: realtorId,
|
||||
})
|
||||
);
|
||||
await rankingService.updateRanking(articles, checkDate);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
18
src/config.ts
Normal file
18
src/config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const realtorIds = ["namyeong00"];
|
||||
export const tradeTypes = ["A1", "B1", "B2", "B3"];
|
||||
export const realestateTypes = [
|
||||
"A01",
|
||||
"A02",
|
||||
"A06",
|
||||
"C01",
|
||||
"C02",
|
||||
"C03",
|
||||
"C04",
|
||||
"D01",
|
||||
"D02",
|
||||
"D03",
|
||||
"D04",
|
||||
"D05",
|
||||
"E01",
|
||||
"E03",
|
||||
];
|
||||
123
src/etc/fetch-articles-pc.ts
Normal file
123
src/etc/fetch-articles-pc.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import axios from "axios";
|
||||
import { realtorIds } from "./config";
|
||||
import axiosRetry from "axios-retry";
|
||||
|
||||
axiosRetry(axios, {
|
||||
retries: 4, // 4번 재시도
|
||||
retryDelay: (retryCount: number) => {
|
||||
console.log(`재시도 ${retryCount}번째 시도 중...`);
|
||||
return retryCount * 2000; // 2초, 4초, 6초, 8초 대기
|
||||
},
|
||||
retryCondition: (error: any) => {
|
||||
// 네트워크 에러 또는 5xx 에러일 때 재시도
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
const startTime = Date.now(); // 시작 시간 기록
|
||||
const realtorId = "namyeong00";
|
||||
|
||||
const headers = {
|
||||
accept: "*/*",
|
||||
"accept-language": "ko;q=0.7",
|
||||
authorization:
|
||||
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlJFQUxFU1RBVEUiLCJpYXQiOjE3NjExMzM1NDUsImV4cCI6MTc2MTE0NDM0NX0.MJv-3xWZeWHCahmq0w5hxlZNmr7qwu1bOEaV2rvuthY",
|
||||
"cache-control": "no-cache",
|
||||
pragma: "no-cache",
|
||||
priority: "u=1, i",
|
||||
referer:
|
||||
"https://new.land.naver.com/houses?ms=37.6560144,126.7916037,15&a=DDDGG:JWJT:SGJT:VL&e=RETAIL&articleNo=2556801691&realtorId=s9055515",
|
||||
"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:
|
||||
"PROP_TEST_ID=7e02e4c10eb2f2c2358bca876c0d7ad0b41ee920553cf666f9f8ffea510fe421; PROP_TEST_KEY=1758430291866.2e161026d60e6da4b6ffc456ba20732ac32391c0294df00f3c0b051eb574277e",
|
||||
};
|
||||
|
||||
// 1. 첫 번째 요청으로 총 개수 가져오기
|
||||
console.log("총 매물 개수 확인 중...");
|
||||
const firstResponse = await axios.get(
|
||||
`https://new.land.naver.com/api/articles?realEstateType=&tradeType=&order=rank&page=1&zoom=0&realtorId=${realtorId}`,
|
||||
{
|
||||
proxy: {
|
||||
host: "gw.dataimpulse.com",
|
||||
port: 823,
|
||||
auth: {
|
||||
username: "0bdeb90b7713c370cdeb__cr.kr",
|
||||
password: "a5ae50d6913bd778",
|
||||
},
|
||||
},
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const mapExposedCount = firstResponse.data.mapExposedCount;
|
||||
console.log(`총 매물 개수: ${mapExposedCount}`);
|
||||
|
||||
// 2. 총 페이지 수 계산 (한 페이지당 20개)
|
||||
const totalPage = Math.ceil(mapExposedCount / 20);
|
||||
console.log(`총 페이지 수: ${totalPage}`);
|
||||
|
||||
// 3. 모든 페이지 번호 배열 생성
|
||||
const pages = Array.from({ length: totalPage }, (_, i) => i + 1);
|
||||
|
||||
// 4. 10개씩 동시 요청
|
||||
const allArticles: any[] = [];
|
||||
const concurrency = 1; // 동시 요청 수
|
||||
|
||||
// 설명
|
||||
for (let i = 0; i < pages.length; i += concurrency) {
|
||||
const chunk = pages.slice(i, i + concurrency);
|
||||
console.log(
|
||||
`페이지 ${chunk[0]} ~ ${chunk[chunk.length - 1]} 요청 중... (${
|
||||
i + 1
|
||||
}-${Math.min(i + concurrency, pages.length)}/${pages.length})`
|
||||
);
|
||||
|
||||
const promises = chunk.map((page) =>
|
||||
axios.get(
|
||||
`https://new.land.naver.com/api/articles?realEstateType=&tradeType=&order=rank&page=${page}&zoom=0&realtorId=${realtorId}`,
|
||||
{
|
||||
proxy: {
|
||||
host: "gw.dataimpulse.com",
|
||||
port: 823,
|
||||
auth: {
|
||||
username: "0bdeb90b7713c370cdeb__cr.kr",
|
||||
password: "a5ae50d6913bd778",
|
||||
},
|
||||
},
|
||||
headers,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
responses.forEach((response, index) => {
|
||||
const articles = response.data.articleList || [];
|
||||
console.log(`페이지 ${chunk[index]}: ${articles.length}개 매물`);
|
||||
allArticles.push(...articles);
|
||||
});
|
||||
|
||||
// 다음 배치 전에 잠시 대기 (API 부하 방지)
|
||||
if (i + concurrency < pages.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now(); // 종료 시간 기록
|
||||
const elapsedTime = ((endTime - startTime) / 1000).toFixed(2); // 초 단위로 변환
|
||||
|
||||
console.log(`\n총 ${allArticles.length}개 매물 수집 완료`);
|
||||
console.log(`소요 시간: ${elapsedTime}초`);
|
||||
console.log(allArticles[0]); // 첫 번째 매물 샘플 출력
|
||||
};
|
||||
|
||||
main();
|
||||
88
src/etc/fetch-detail-disabled.ts
Normal file
88
src/etc/fetch-detail-disabled.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NaverRealEstate } from "./src/services/naver.service";
|
||||
import prisma from "./src/lib/prisma";
|
||||
|
||||
async function main() {
|
||||
const realtorId = "a7062525";
|
||||
|
||||
const naver = new NaverRealEstate({
|
||||
realtorId: realtorId,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("활성 매물 목록 가져오는 중...");
|
||||
|
||||
// DB에서 isActive가 true이고 realtorId가 일치하는 매물 가져오기
|
||||
const activeArticles = await prisma.realEstateArticle.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
realtorId: realtorId,
|
||||
},
|
||||
select: {
|
||||
articleNumber: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`총 ${activeArticles.length}개의 활성 매물 발견`);
|
||||
|
||||
if (activeArticles.length === 0) {
|
||||
console.log("처리할 매물이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 매물의 상세 정보 가져오기
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (let i = 0; i < activeArticles.length; i++) {
|
||||
const article = activeArticles[i];
|
||||
if (!article) {
|
||||
continue;
|
||||
}
|
||||
console.log(
|
||||
`\n[${i + 1}/${activeArticles.length}] ${
|
||||
article.articleNumber
|
||||
} 처리 중...`
|
||||
);
|
||||
|
||||
try {
|
||||
// 상세 정보 HTML 가져오기 (프록시 사용)
|
||||
const detailHtml = await naver.getArticleDetailWithProxy(
|
||||
article.articleNumber
|
||||
);
|
||||
|
||||
// HTML에서 JSON 데이터 추출
|
||||
const detailData = naver.parseArticleDetailFromHtml(detailHtml);
|
||||
|
||||
if (detailData) {
|
||||
// DB에 상세 정보 저장
|
||||
await naver.saveArticleDetailToDB(article.articleNumber, detailData);
|
||||
successCount++;
|
||||
console.log(`✅ 저장 완료`);
|
||||
} else {
|
||||
failCount++;
|
||||
console.log(`❌ 데이터 파싱 실패`);
|
||||
}
|
||||
|
||||
// 요청 간격 (1~2초 랜덤)
|
||||
const waitTime = 1000 + Math.floor(Math.random() * 1000);
|
||||
console.log(`${waitTime}ms 대기...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
console.error(`❌ 오류 발생:`, error);
|
||||
|
||||
// 오류 발생 시 더 긴 대기
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n=== 처리 완료 ===");
|
||||
console.log(`성공: ${successCount}개`);
|
||||
console.log(`실패: ${failCount}개`);
|
||||
} catch (error) {
|
||||
console.error("오류 발생:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 실행
|
||||
main();
|
||||
51
src/fetch-articles.ts
Normal file
51
src/fetch-articles.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NaverRealEstate } from "./services/naver.service";
|
||||
import { realtorIds } from "./config";
|
||||
|
||||
// 사용 예제
|
||||
async function main() {
|
||||
for (const realtorId of realtorIds) {
|
||||
const naver = new NaverRealEstate({
|
||||
realtorId: realtorId,
|
||||
});
|
||||
try {
|
||||
let cookie = await naver.getApiCookie();
|
||||
// 2. DB에서 Seed 불러오기 시도
|
||||
console.log("Seed 불러오는 중...");
|
||||
let seed = await naver.fetchSeed(cookie);
|
||||
|
||||
console.log("Seed:", seed);
|
||||
|
||||
await naver.resetActiveStatus();
|
||||
|
||||
// 3. 등록된 매물(Article) 목록 가져오기 (자동으로 DB에 저장됨)
|
||||
console.log("\n매물 목록 가져오는 중...");
|
||||
const articles = await naver.getArticlesAndSave();
|
||||
console.log(`\n총 ${articles.length}개의 매물 발견`);
|
||||
|
||||
// 삭제된 매물 삭제
|
||||
await naver.deleteUnactiveArticles();
|
||||
|
||||
// 삭제되지 않은 매물 가져옴
|
||||
let activeArticles = await naver.getActiveArticles(realtorId);
|
||||
activeArticles = activeArticles.filter(
|
||||
(article) => article.brokerName === null
|
||||
);
|
||||
|
||||
console.log(`\n총 ${activeArticles.length}개의 매물 발견`);
|
||||
|
||||
for (let i = 0; i < activeArticles.length; i += 30) {
|
||||
const batch = activeArticles.slice(i, i + 30);
|
||||
await Promise.all(
|
||||
batch.map(async (article) => {
|
||||
await naver.updateBrokerInfo(article.articleNumber, cookie);
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("오류 발생:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 실행
|
||||
main();
|
||||
26
src/fetch-detailAddress.ts
Normal file
26
src/fetch-detailAddress.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NaverRealEstate } from "./services/naver.service";
|
||||
|
||||
async function main() {
|
||||
const realtorIds = ["namyeong00"];
|
||||
|
||||
console.log("detailAddress 없는 매물 목록 가져오는 중...");
|
||||
|
||||
for (const realtorId of realtorIds) {
|
||||
const naver = new NaverRealEstate({
|
||||
realtorId: realtorId,
|
||||
});
|
||||
try {
|
||||
// 상세 정보 HTML 가져오기 (프록시 사용)
|
||||
await naver.updateArticleDetaiAddressWithProxy();
|
||||
console.log("완료");
|
||||
} catch (error) {
|
||||
console.error(`❌ 오류 발생:`, error);
|
||||
|
||||
// 오류 발생 시 더 긴 대기
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 실행
|
||||
main();
|
||||
@@ -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 반환
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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 || "",
|
||||
},
|
||||
|
||||
59
src/starter.ts
Normal file
59
src/starter.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import schedule from "node-schedule";
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
console.log("start ...");
|
||||
|
||||
function runTsFile(filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Starting: ${filePath}`);
|
||||
exec(`bun run ${filePath}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Execution error: ${error.message}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
}
|
||||
console.log(`Output: ${stdout}`);
|
||||
console.log(`Completed: ${filePath}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 10시에 실행 - fetch-articles.ts 실행 후 app.ts 실행
|
||||
schedule.scheduleJob("0 0 10 * * *", async () => {
|
||||
console.log("Running parserLandList.ts at 10:00 AM");
|
||||
try {
|
||||
await runTsFile("./fetch-articles.ts");
|
||||
// fetch-articles.ts 완료 후 app.ts 실행
|
||||
await runTsFile("./fetch-detailAddress.ts");
|
||||
} catch (error) {
|
||||
console.error("Error in scheduled job:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 11시에 실행
|
||||
schedule.scheduleJob("0 11 * * *", () => {
|
||||
console.log("Running app.ts at 11 AM");
|
||||
runTsFile("./app.ts");
|
||||
});
|
||||
|
||||
// 15시에 실행 - fetch-articles.ts 실행 후 app.ts 실행
|
||||
schedule.scheduleJob("0 0 15 * * *", async () => {
|
||||
console.log("Running parserLandList.ts at 3:00 PM");
|
||||
try {
|
||||
await runTsFile("./fetch-articles.ts");
|
||||
// fetch-articles.ts 완료 후 app.ts 실행
|
||||
await runTsFile("./fetch-detailAddress.ts");
|
||||
} catch (error) {
|
||||
console.error("Error in scheduled job:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 16시에 실행
|
||||
schedule.scheduleJob("0 16 * * *", () => {
|
||||
console.log("Running app.ts at 4 PM");
|
||||
runTsFile("./app.ts");
|
||||
});
|
||||
Reference in New Issue
Block a user