Skip to content

Commit

Permalink
Merge pull request #125 from boostcampwm-2024/feat/trend-sse
Browse files Browse the repository at this point in the history
✨ feat: 트랜드 스케쥴러, SSE 구현
  • Loading branch information
Jo-Minseok authored Nov 20, 2024
2 parents 51e0072 + f2e18df commit d03b39c
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 79 deletions.
88 changes: 85 additions & 3 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.1.1",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^8.0.1",
"@nestjs/typeorm": "^10.0.2",
"bcrypt": "^5.1.1",
Expand All @@ -33,6 +35,7 @@
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"mysql2": "^3.11.3",
"nest-winston": "^1.9.7",
"nodemailer": "^6.9.16",
Expand All @@ -51,6 +54,7 @@
"@types/cookie-parser": "^1.4.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16",
"@types/supertest": "^6.0.0",
Expand Down
60 changes: 60 additions & 0 deletions server/src/feed/feed.api-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,63 @@ export function ApiGetTrendList() {
}),
);
}

export function ApiGetTrendSse() {
return applyDecorators(
ApiOperation({
summary: '트랜드 게시글 조회 SSE',
}),
ApiOkResponse({
description: 'Ok',
schema: {
example: {
message: '트렌드 피드 수신 완료',
data: [
{
id: 1,
author: '안성윤',
title:
'자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)',
path: 'https://asn6878.tistory.com/9',
createdAt: '2022-09-05 09:00:00',
thumbnail:
'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png',
viewCount: 0,
},
{
id: 2,
author: '조민석',
title:
'[네이버 커넥트재단 부스트캠프 웹・모바일 9기] 날 것 그대로 작성하는 챌린지 수료 후기 - Web',
path: 'https://velog.io/@seok3765/%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%BB%A4%EB%84%A5%ED%8A%B8%EC%9E%AC%EB%8B%A8-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%E3%83%BB%EB%AA%A8%EB%B0%94%EC%9D%BC-9%EA%B8%B0-%EB%82%A0-%EA%B2%83-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%B1%8C%EB%A6%B0%EC%A7%80-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0-Web',
createdAt: '2024-08-14 14:07:49',
thumbnail:
'https://velog.velcdn.com/images/seok3765/post/2f863481-b594-46f8-9a28-7799afb58aa4/image.jpg',
viewCount: 0,
},
{
id: 3,
author: '박무성',
title: '제목',
path: 'https://asn6878.tistory.com/9',
createdAt: '2022-09-05 09:00:00',
thumbnail:
'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png',
viewCount: 0,
},
{
id: 4,
author: '박무성',
title: '제목',
path: 'https://asn6878.tistory.com/9',
createdAt: '2022-09-05 10:00:00',
thumbnail:
'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png',
viewCount: 0,
},
],
},
},
}),
);
}
26 changes: 24 additions & 2 deletions server/src/feed/feed.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ import {
HttpCode,
HttpStatus,
Query,
Sse,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { FeedService } from './feed.service';
import { QueryFeedDto } from './dto/query-feed.dto';
import { SearchFeedReq } from './dto/search-feed.dto';
import {
ApiGetFeedList,
ApiSearchFeed,
ApiGetTrendList,
ApiGetTrendSse,
} from './feed.api-docs';
import { SearchFeedReq } from './dto/search-feed.dto';
import { Observable } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter';

@ApiTags('Feed')
@Controller('feed')
export class FeedController {
constructor(private readonly feedService: FeedService) {}
constructor(
private readonly feedService: FeedService,
private readonly eventService: EventEmitter2,
) {}

@ApiGetFeedList()
@Get('')
Expand All @@ -45,6 +52,21 @@ export class FeedController {
return ApiResponse.responseWithData('트렌드 피드 조회 완료', responseData);
}

@ApiGetTrendSse()
@Sse('trend/sse')
async sseTrendList() {
return new Observable((observer) => {
this.eventService.on('ranking-update', (trendData) => {
observer.next({
data: {
message: '트렌드 피드 수신 완료',
trendData,
},
});
});
});
}

@ApiSearchFeed()
@Get('search')
@HttpCode(HttpStatus.OK)
Expand Down
8 changes: 7 additions & 1 deletion server/src/feed/feed.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { Feed } from './feed.entity';
import { FeedController } from './feed.controller';
import { FeedService } from './feed.service';
import { FeedRepository } from './feed.repository';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
imports: [TypeOrmModule.forFeature([Feed])],
imports: [
TypeOrmModule.forFeature([Feed]),
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
],
controllers: [FeedController],
providers: [FeedService, FeedRepository],
})
Expand Down
27 changes: 26 additions & 1 deletion server/src/feed/feed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { FeedRepository } from './feed.repository';
import { QueryFeedDto } from './dto/query-feed.dto';
import { FeedResponseDto } from './dto/feed-response.dto';
import { RedisService } from '../common/redis/redis.service';
import { Cron, CronExpression } from '@nestjs/schedule';
import { EventEmitter2 } from '@nestjs/event-emitter';
import * as _ from 'lodash';
import { Feed } from './feed.entity';
import {
SearchFeedReq,
Expand All @@ -16,6 +19,7 @@ export class FeedService {
constructor(
private readonly feedRepository: FeedRepository,
private readonly redisService: RedisService,
private readonly eventService: EventEmitter2,
) {}

async getFeedData(queryFeedDto: QueryFeedDto) {
Expand All @@ -38,7 +42,7 @@ export class FeedService {
}

async getTrendList() {
const trendFeedIdList = await this.redisService.redisClient.zrange(
const trendFeedIdList = await this.redisService.redisClient.zrevrange(
'feed:trend',
0,
3,
Expand All @@ -57,6 +61,27 @@ export class FeedService {
return trendFeeds.filter((feed) => feed !== null);
}

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async resetTrendTable() {
await this.redisService.redisClient.del('feed:trend');
}

@Cron(CronExpression.EVERY_MINUTE)
async analyzeTrend() {
const [originTrend, nowTrend] = await Promise.all([
this.redisService.redisClient.lrange('feed:origin_trend', 0, 3),
this.redisService.redisClient.zrevrange('feed:trend', 0, 3),
]);
if (!_.isEqual(originTrend, nowTrend)) {
const redisPipeline = this.redisService.redisClient.pipeline();
redisPipeline.del('feed:origin_trend');
redisPipeline.rpush('feed:origin_trend', ...nowTrend);
await redisPipeline.exec();
const trendFeeds = await this.getTrendList();
this.eventService.emit('ranking-update', trendFeeds);
}
}

async search(searchFeedReq: SearchFeedReq) {
console.log(typeof searchFeedReq.page);

Expand Down
Loading

0 comments on commit d03b39c

Please sign in to comment.