Skip to content

Commit

Permalink
Merge branch 'main' into refactor/rss-api-performance
Browse files Browse the repository at this point in the history
  • Loading branch information
Jo-Minseok committed Dec 4, 2024
2 parents 4f444aa + 1fefa7d commit 618d0e4
Show file tree
Hide file tree
Showing 24 changed files with 1,165 additions and 471 deletions.
34 changes: 24 additions & 10 deletions rss-notifier/src/common/db-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Redis from "ioredis";
import * as process from "node:process";
import { CONNECTION_LIMIT, INSERT_ID, redisConstant } from "./constant";
import * as dotenv from "dotenv";
import { PoolConnection } from "mysql2/promise";

dotenv.config({
path: process.env.NODE_ENV === "production" ? "rss-notifier/.env" : ".env",
Expand All @@ -27,12 +28,12 @@ export const createRedisClient = async () => {
});
};

export const executeQuery = async (query: string, params: any[] = []) => {
let connection;
export const executeQuery = async <T>(query: string, params: any[] = []) => {
let connection: PoolConnection;
try {
connection = await pool.getConnection();
const [rows] = await connection.query(query, params);
return rows;
return rows as T[];
} catch (err) {
logger.error("쿼리 " + query + " 실행 중 오류 발생:", err);
throw err;
Expand All @@ -51,15 +52,15 @@ export const selectAllRss = async (): Promise<RssObj[]> => {

export const insertFeeds = async (resultData: FeedDetail[]) => {
let successCount = 0;
let lastFeedId;
let lastFeedId: number;
const query = `
INSERT INTO feed (blog_id, created_at, title, path, thumbnail)
VALUES (?, ?, ?, ?, ?)
`;
for (const feed of resultData) {
try {
lastFeedId = (
await executeQuery(query, [
await executeQuery<FeedDetail>(query, [
feed.blogId,
feed.pubDate,
feed.title,
Expand All @@ -80,12 +81,25 @@ export const insertFeeds = async (resultData: FeedDetail[]) => {
return lastFeedId;
};

export const deleteRecentFeedStartId = async () => {
export const deleteRecentFeed = async () => {
const redis = await createRedisClient();
try {
const keys = await redis.keys(redisConstant.FEED_RECENT_ALL_KEY);
if (keys.length > 0) {
await redis.del(...keys);
const keysToDelete = [];
let cursor = "0";
do {
const [newCursor, keys] = await redis.scan(
cursor,
"MATCH",
redisConstant.FEED_RECENT_ALL_KEY,
"COUNT",
"100"
);
keysToDelete.push(...keys);
cursor = newCursor;
} while (cursor !== "0");

if (keysToDelete.length > 0) {
await redis.del(...keysToDelete);
}
await redis.set(redisConstant.FEED_RECENT_KEY, "false");
} catch (error) {
Expand Down Expand Up @@ -113,7 +127,7 @@ export const setRecentFeedList = async (startId: number) => {
FROM feed f
JOIN rss_accept r ON f.blog_id = r.id
WHERE f.id >= ${startId}`;
const resultList = await executeQuery(query);
const resultList = await executeQuery<RssObj>(query);
const pipeLine = redis.pipeline();
for (const feed of resultList) {
pipeLine.hset(`feed:recent:${feed.id}`, feed);
Expand Down
4 changes: 2 additions & 2 deletions rss-notifier/src/rss-notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import "dotenv/config";
import {
selectAllRss,
insertFeeds,
deleteRecentFeedStartId,
setRecentFeedList,
deleteRecentFeed,
} from "./common/db-access.js";
import { RssObj, FeedDetail, RawFeed } from "./common/types.js";
import { XMLParser } from "fast-xml-parser";
Expand Down Expand Up @@ -135,9 +135,9 @@ export const performTask = async () => {
return dateCurrent.getTime() - dateNext.getTime();
});

await deleteRecentFeed();
if (result.length === 0) {
logger.info("새로운 피드가 없습니다.");
await deleteRecentFeedStartId();
return;
}

Expand Down
2 changes: 1 addition & 1 deletion server/src/admin/admin.api-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function ApiPostRegisterAdmin() {
ApiConflictResponse({
description: 'Conflict',
example: {
message: '이미 존재하는 계정입니다.',
message: '이미 존재하는 아이디입니다.',
},
}),
ApiUnauthorizedResponse({
Expand Down
8 changes: 4 additions & 4 deletions server/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class AdminService {
private readonly SESSION_TTL = 60 * 60 * 12;

constructor(
private readonly loginRepository: AdminRepository,
private readonly adminRepository: AdminRepository,
private readonly redisService: RedisService,
) {}

Expand All @@ -30,7 +30,7 @@ export class AdminService {
const cookie = request.cookies['sessionId'];
const { loginId, password } = loginAdminDto;

const admin = await this.loginRepository.findOne({
const admin = await this.adminRepository.findOne({
where: { loginId },
});

Expand Down Expand Up @@ -95,7 +95,7 @@ export class AdminService {
async registerAdmin(registerAdminDto: RegisterAdminDto) {
let { loginId, password } = registerAdminDto;

const existingAdmin = await this.loginRepository.findOne({
const existingAdmin = await this.adminRepository.findOne({
where: { loginId },
});

Expand All @@ -106,6 +106,6 @@ export class AdminService {
const saltRounds = 10;
password = await bcrypt.hash(password, saltRounds);

return this.loginRepository.registerAdmin({ loginId, password });
return this.adminRepository.registerAdmin({ loginId, password });
}
}
88 changes: 88 additions & 0 deletions server/src/feed/api-docs/readFeedList.api-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
} from '@nestjs/swagger';

export function ApiReadFeedList() {
return applyDecorators(
ApiOperation({
summary: `메인 화면 게시글 조회 API`,
}),
ApiQuery({
name: 'lastId',
required: false,
type: Number,
description: '마지막으로 받은 피드의 ID',
example: 10,
}),
ApiQuery({
name: 'limit',
required: false,
type: Number,
description: '한 번에 가져올 피드 수',
example: 5,
default: 12,
}),
ApiOkResponse({
description: 'Ok',
schema: {
properties: {
message: {
type: 'string',
},
data: {
type: 'object',
properties: {
result: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
author: { type: 'string' },
blogPlatform: { type: 'string' },
title: { type: 'string' },
path: { type: 'string', format: 'url' },
createAt: { type: 'string', format: 'date-time' },
thumbnail: { type: 'string', format: 'url' },
viewCount: { type: 'number' },
},
},
},
lastId: { type: 'number' },
hasMore: { type: 'boolean' },
},
},
},
},
example: {
message: '피드 조회 완료',
data: {
result: [
{
id: 3,
author: '블로그 이름',
blogPlatform: '블로그 서비스 플랫폼',
title: '피드 제목',
path: 'https://test.com',
createAt: '2024-06-16T20:00:57.000Z',
thumbnail: 'https://test.com/image.png',
viewCount: 1,
},
],
lastId: 3,
hasMore: true,
},
},
}),
ApiBadRequestResponse({
description: 'Bad Request',
example: {
message: '오류 메세지',
},
}),
);
}
54 changes: 54 additions & 0 deletions server/src/feed/api-docs/readRecentFeedList.api-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse, ApiOperation } from '@nestjs/swagger';

export function ApiReadRecentFeedList() {
return applyDecorators(
ApiOperation({
summary: '최신 피드 업데이트 API',
}),
ApiOkResponse({
description: 'Ok',
schema: {
properties: {
message: {
type: 'string',
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
author: { type: 'string' },
blogPlatform: { type: 'string' },
title: { type: 'string' },
path: { type: 'string' },
createdAt: {
type: 'string',
format: 'date-time',
},
thumbnail: { type: 'string' },
viewCount: { type: 'number' },
},
},
},
},
},
example: {
message: '최신 피드 업데이트 완료',
data: [
{
id: 1,
author: '블로그 이름',
blogPlatform: 'etc',
title: '게시글 제목',
path: 'https://test1.com/1',
createdAt: '2024-11-24T01:00:00.000Z',
thumbnail: 'https://test1.com/test.png',
viewCount: 0,
},
],
},
}),
);
}
97 changes: 97 additions & 0 deletions server/src/feed/api-docs/readTrendFeedList.api-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse, ApiOperation } from '@nestjs/swagger';

export function ApiReadTrendFeedList() {
return applyDecorators(
ApiOperation({
summary: '트렌드 게시글 조회 SSE',
}),
ApiOkResponse({
description: 'Ok',
schema: {
properties: {
message: {
type: 'string',
},
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
author: { type: 'string' },
blogPlatform: { type: 'string' },
title: { type: 'string' },
path: { type: 'string' },
createdAt: {
type: 'string',
format: 'date-time',
},
thumbnail: { type: 'string' },
viewCount: { type: 'number' },
},
},
},
},
},
examples: {
connect: {
summary: '현재 트렌드 피드 수신 완료',
value: {
message: '현재 트렌드 피드 수신 완료',
data: [
{
id: 1,
author: '블로그 이름',
blogPlatform: '블로그 서비스 플랫폼',
title: '피드 제목',
path: 'https://test1.com/1',
createdAt: '2024-11-24T01:00:00.000Z',
thumbnail: 'https://test1.com/test.png',
viewCount: 0,
},
{
id: 2,
author: '블로그 이름',
blogPlatform: '블로그 서비스 플랫폼',
title: '피드 제목',
path: 'https://test2.com/1',
createdAt: '2024-11-24T02:00:00.000Z',
thumbnail: 'https://test2.com/test.png',
viewCount: 0,
},
],
},
},
continue: {
summary: '새로운 트렌드 피드 수신 완료',
value: {
message: '새로운 트렌드 피드 수신 완료',
data: [
{
id: 3,
author: '블로그 이름',
blogPlatform: '블로그 서비스 플랫폼',
title: '피드 제목',
path: 'https://test3.com/1',
createdAt: '2024-11-24T03:00:00.000Z',
thumbnail: 'https://test3.com/test.png',
viewCount: 0,
},
{
id: 4,
author: '블로그 이름',
blogPlatform: '블로그 서비스 플랫폼',
title: '피드 제목',
path: 'https://test4.com/1',
createdAt: '2024-11-24T04:00:00.000Z',
thumbnail: 'https://test4.com/test.png',
viewCount: 0,
},
],
},
},
},
}),
);
}
Loading

0 comments on commit 618d0e4

Please sign in to comment.