첫 완료

This commit is contained in:
kjy
2025-11-12 02:58:36 +09:00
commit 2aab5be513
57 changed files with 56098 additions and 0 deletions

18
src/lib/prisma.ts Normal file
View 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;

File diff suppressed because it is too large Load Diff

View 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 };
}
}
}

View 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
View 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[];
}