첫 완료
This commit is contained in:
18
src/lib/prisma.ts
Normal file
18
src/lib/prisma.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PrismaClient } from "../generated/prisma";
|
||||
|
||||
// Prisma Client 싱글톤 인스턴스
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: ["error", "warn"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
export default prisma;
|
||||
1114
src/services/naver.service.ts
Normal file
1114
src/services/naver.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
56
src/services/ranking.service.ts
Normal file
56
src/services/ranking.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { RealEstateArticle } from "../generated/prisma";
|
||||
import type { NaverRealEstate } from "./naver.service";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
export class RankingService {
|
||||
private naver: NaverRealEstate;
|
||||
constructor(naver: NaverRealEstate) {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
async updateCortarNoAndLgeo(article: RealEstateArticle): Promise<{
|
||||
cortarNo: string | null;
|
||||
lgeo: string | null;
|
||||
}> {
|
||||
try {
|
||||
const { cortarNo, lgeo } = await this.naver.getCortarNoAndLgeo(article);
|
||||
|
||||
await prisma.realEstateArticle.update({
|
||||
where: { id: article.id },
|
||||
data: { cortarNo: cortarNo ?? null, lgeo: lgeo ?? null },
|
||||
});
|
||||
return { cortarNo: cortarNo ?? null, lgeo: lgeo ?? null };
|
||||
} catch (error) {
|
||||
console.error(`❌ 오류 발생:`, error);
|
||||
return { cortarNo: null, lgeo: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/services/telegram.service.ts
Normal file
223
src/services/telegram.service.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import TelegramBot from "node-telegram-bot-api";
|
||||
|
||||
export interface TelegramUser {
|
||||
site: string;
|
||||
chatId: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
realtorId: string;
|
||||
}
|
||||
|
||||
export const telegramUsers: TelegramUser[] = [
|
||||
{
|
||||
site: "선방",
|
||||
chatId: 6824763190,
|
||||
name: "이성원",
|
||||
phone: "010-8098-0254",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "ALL",
|
||||
chatId: 6843597951,
|
||||
name: "강승원",
|
||||
phone: "010-5947-0000",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산써브",
|
||||
chatId: 6876605367,
|
||||
name: "지주완",
|
||||
phone: "010-2716-0987",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산뱅크",
|
||||
chatId: 6876605367,
|
||||
name: "지주완",
|
||||
phone: "010-2716-0987",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산써브",
|
||||
chatId: 5313195485,
|
||||
name: "지주완",
|
||||
phone: "010-6377-2069",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산써브",
|
||||
chatId: 6864925398,
|
||||
name: "박희영",
|
||||
phone: "010-5387-4521",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산포스",
|
||||
chatId: 8155003662,
|
||||
name: "국성혜",
|
||||
phone: "010-8305-1291",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "부동산포스",
|
||||
chatId: 8329969238,
|
||||
name: "이경희",
|
||||
phone: "010-6346-0996",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
{
|
||||
site: "산업부동산",
|
||||
chatId: 8358690326,
|
||||
name: "정명진",
|
||||
phone: "010-4199-9650",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
];
|
||||
|
||||
export const testUsers: TelegramUser[] = [
|
||||
{
|
||||
site: "ALL",
|
||||
chatId: 141033632,
|
||||
name: "김제연",
|
||||
phone: "010-8873-8711",
|
||||
realtorId: "namyeong00",
|
||||
},
|
||||
];
|
||||
|
||||
export class TelegramService {
|
||||
private bot: TelegramBot;
|
||||
private token: string;
|
||||
|
||||
constructor(token: string, enablePolling: boolean = false) {
|
||||
this.token = token;
|
||||
this.bot = new TelegramBot(token, { polling: enablePolling });
|
||||
|
||||
if (enablePolling) {
|
||||
this.setupListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 봇 리스너 설정
|
||||
*/
|
||||
private setupListeners() {
|
||||
// 모든 메시지 처리
|
||||
this.bot.on("message", (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
console.log(`Message received from chatId: ${chatId}`);
|
||||
this.sendMessage(chatId, "Received your message");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메시지 전송
|
||||
*/
|
||||
async sendMessage(chatId: number, message: string): Promise<void> {
|
||||
try {
|
||||
await this.bot.sendMessage(chatId, message);
|
||||
console.log(`✅ 메시지 전송 성공 - chatId: ${chatId}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 메시지 전송 실패 - chatId: ${chatId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 전송
|
||||
*/
|
||||
async sendDocument(
|
||||
chatId: number,
|
||||
fileName: string,
|
||||
caption?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.bot.sendDocument(
|
||||
chatId,
|
||||
fileName,
|
||||
{
|
||||
caption: caption || "",
|
||||
},
|
||||
{
|
||||
filename: fileName.split("/").pop(),
|
||||
contentType:
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
}
|
||||
);
|
||||
console.log(`✅ 문서 전송 성공 - chatId: ${chatId}, file: ${fileName}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ 문서 전송 실패 - chatId: ${chatId}, file: ${fileName}`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사진 전송
|
||||
*/
|
||||
async sendPhoto(
|
||||
chatId: number,
|
||||
photo: string | Buffer,
|
||||
caption?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.bot.sendPhoto(chatId, photo, {
|
||||
caption: caption || "",
|
||||
});
|
||||
console.log(`✅ 사진 전송 성공 - chatId: ${chatId}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 사진 전송 실패 - chatId: ${chatId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 사용자에게 메시지 전송
|
||||
*/
|
||||
async broadcastMessage(
|
||||
users: TelegramUser[],
|
||||
message: string
|
||||
): Promise<void> {
|
||||
const promises = users.map((user) =>
|
||||
this.sendMessage(user.chatId, message).catch((error) => {
|
||||
console.error(
|
||||
`❌ ${user.name}(${user.chatId})에게 메시지 전송 실패:`,
|
||||
error
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log(`✅ ${users.length}명에게 메시지 브로드캐스트 완료`);
|
||||
}
|
||||
|
||||
/**
|
||||
* realtorId로 사용자 필터링
|
||||
*/
|
||||
getUsersByRealtorId(
|
||||
users: TelegramUser[],
|
||||
realtorId: string
|
||||
): TelegramUser[] {
|
||||
return users.filter((user) => user.realtorId === realtorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* site로 사용자 필터링
|
||||
*/
|
||||
getUsersBySite(users: TelegramUser[], site: string): TelegramUser[] {
|
||||
return users.filter((user) => user.site === site || user.site === "ALL");
|
||||
}
|
||||
|
||||
/**
|
||||
* 봇 중지
|
||||
*/
|
||||
stopPolling(): void {
|
||||
this.bot.stopPolling();
|
||||
console.log("✅ 텔레그램 봇 폴링 중지");
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 토큰으로 싱글톤 인스턴스 생성
|
||||
const DEFAULT_TOKEN = "233460568:AAHWgRQo5IgcWR0uXdsiMEzNnsmIqjOgk24";
|
||||
export const telegramService = new TelegramService(DEFAULT_TOKEN, false);
|
||||
80
src/types/naver.types.ts
Normal file
80
src/types/naver.types.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
export interface ArticleItem {
|
||||
articleNumber: string;
|
||||
articleName: string;
|
||||
buildingType: string;
|
||||
tradeType: string;
|
||||
realEstateType: string;
|
||||
spaceInfo: {
|
||||
supplySpace: number;
|
||||
exclusiveSpace: number;
|
||||
landSpace: number;
|
||||
exclusiveSpaceName: string;
|
||||
};
|
||||
buildingInfo: {
|
||||
buildingConjunctionDateType: string;
|
||||
buildingConjunctionDate: string;
|
||||
approvalElapsedYear: number;
|
||||
};
|
||||
verificationInfo: {
|
||||
verificationType: string;
|
||||
isAssociationArticle: boolean;
|
||||
exposureStartDate: string;
|
||||
articleConfirmDate: string;
|
||||
};
|
||||
articleDetail: {
|
||||
direction: string;
|
||||
directionStandard: string;
|
||||
articleFeatureDescription: string;
|
||||
directTrade: boolean;
|
||||
floorInfo: string;
|
||||
floorDetailInfo: {
|
||||
targetFloor: string;
|
||||
totalFloor: string;
|
||||
groundTotalFloor: string;
|
||||
undergroundTotalFloor: string;
|
||||
floorType: string;
|
||||
};
|
||||
isSafeLessorOfHug: boolean;
|
||||
};
|
||||
articleMedia: {
|
||||
imageUrl: string;
|
||||
imageType: string;
|
||||
imageCount: number;
|
||||
isVrExposed: boolean;
|
||||
};
|
||||
address: {
|
||||
city: string;
|
||||
division: string;
|
||||
sector: string;
|
||||
coordinates: {
|
||||
xCoordinate: number;
|
||||
yCoordinate: number;
|
||||
};
|
||||
subwayInfo: any;
|
||||
};
|
||||
priceInfo: {
|
||||
dealPrice: number;
|
||||
warrantyPrice: number;
|
||||
rentPrice: number;
|
||||
managementFeeAmount: number;
|
||||
priceChangeStatus: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ArticleResponse {
|
||||
seed: string;
|
||||
lastInfo: (string | number)[];
|
||||
hasNextPage: boolean;
|
||||
list: Array<{
|
||||
representativeArticleInfo: ArticleItem;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface NaverRealEstateConfig {
|
||||
realtorId: string;
|
||||
seed?: string;
|
||||
baseUrl?: string;
|
||||
tradeTypes?: string[];
|
||||
realestateTypes?: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user