diff --git a/server/package-lock.json b/server/package-lock.json index 53e42275..3b3aa4b8 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,7 +12,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", @@ -21,6 +23,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", @@ -39,6 +42,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", @@ -1885,6 +1889,25 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.1.1.tgz", + "integrity": "sha512-6L6fBOZTyfFlL7Ih/JDdqlCzZeCW0RjCX28wnzGyg/ncv5F/EOeT1dfopQr1loBRQ3LTgu8OWM7n4zLN4xigsg==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/event-emitter/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", @@ -1926,6 +1949,33 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", + "license": "MIT", + "dependencies": { + "cron": "3.1.7", + "uuid": "10.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2650,6 +2700,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -4761,6 +4824,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cron": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" + } + }, "node_modules/croner": { "version": "4.1.97", "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", @@ -4786,9 +4859,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8243,6 +8316,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", diff --git a/server/package.json b/server/package.json index 03a0ade1..76988dc9 100644 --- a/server/package.json +++ b/server/package.json @@ -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", @@ -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", @@ -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", diff --git a/server/src/feed/feed.api-docs.ts b/server/src/feed/feed.api-docs.ts index 22f5d247..181b8637 100644 --- a/server/src/feed/feed.api-docs.ts +++ b/server/src/feed/feed.api-docs.ts @@ -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, + }, + ], + }, + }, + }), + ); +} diff --git a/server/src/feed/feed.controller.ts b/server/src/feed/feed.controller.ts index 241da24f..1033c1dd 100644 --- a/server/src/feed/feed.controller.ts +++ b/server/src/feed/feed.controller.ts @@ -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('') @@ -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) diff --git a/server/src/feed/feed.module.ts b/server/src/feed/feed.module.ts index 09138e34..482f0c03 100644 --- a/server/src/feed/feed.module.ts +++ b/server/src/feed/feed.module.ts @@ -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], }) diff --git a/server/src/feed/feed.service.ts b/server/src/feed/feed.service.ts index 33991811..2ee214bc 100644 --- a/server/src/feed/feed.service.ts +++ b/server/src/feed/feed.service.ts @@ -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, @@ -16,6 +19,7 @@ export class FeedService { constructor( private readonly feedRepository: FeedRepository, private readonly redisService: RedisService, + private readonly eventService: EventEmitter2, ) {} async getFeedData(queryFeedDto: QueryFeedDto) { @@ -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, @@ -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); diff --git a/server/test/feed/trend.e2e-spec.ts b/server/test/feed/trend.e2e-spec.ts index 8a6a9abd..5464889d 100644 --- a/server/test/feed/trend.e2e-spec.ts +++ b/server/test/feed/trend.e2e-spec.ts @@ -98,16 +98,6 @@ describe('Trend API', () => { viewCount: 0, blog: blogs[2], }, - { - id: 5, - title: '제목', - path: 'https://asn6878.tistory.com/12', - createdAt: '2022-09-05T10:00:00.000Z', - thumbnail: - 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', - viewCount: 0, - blog: blogs[2], - }, ]); redisService = app.get(RedisService); }); @@ -168,17 +158,6 @@ describe('Trend API', () => { expect(response.body).toStrictEqual({ message: '트렌드 피드 조회 완료', data: [ - { - id: 1, - author: '안성윤', - title: - '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', - path: 'https://asn6878.tistory.com/9', - createdAt: '2022-09-05T09:00:00.000Z', - 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: '조민석', @@ -190,6 +169,17 @@ describe('Trend API', () => { 'https://velog.velcdn.com/images/seok3765/post/2f863481-b594-46f8-9a28-7799afb58aa4/image.jpg', viewCount: 0, }, + { + id: 1, + author: '안성윤', + title: + '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', + path: 'https://asn6878.tistory.com/9', + createdAt: '2022-09-05T09:00:00.000Z', + thumbnail: + 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', + viewCount: 0, + }, ], }); }); @@ -207,11 +197,10 @@ describe('Trend API', () => { message: '트렌드 피드 조회 완료', data: [ { - id: 1, - author: '안성윤', - title: - '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', - path: 'https://asn6878.tistory.com/9', + id: 3, + author: '박무성', + title: '제목', + path: 'https://asn6878.tistory.com/10', createdAt: '2022-09-05T09:00:00.000Z', thumbnail: 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', @@ -229,10 +218,11 @@ describe('Trend API', () => { viewCount: 0, }, { - id: 3, - author: '박무성', - title: '제목', - path: 'https://asn6878.tistory.com/10', + id: 1, + author: '안성윤', + title: + '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', + path: 'https://asn6878.tistory.com/9', createdAt: '2022-09-05T09:00:00.000Z', thumbnail: 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', @@ -256,11 +246,20 @@ describe('Trend API', () => { message: '트렌드 피드 조회 완료', data: [ { - id: 1, - author: '안성윤', - title: - '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', - path: 'https://asn6878.tistory.com/9', + id: 4, + author: '박무성', + title: '제목', + path: 'https://asn6878.tistory.com/11', + createdAt: '2022-09-05T10:00:00.000Z', + 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: 3, + author: '박무성', + title: '제목', + path: 'https://asn6878.tistory.com/10', createdAt: '2022-09-05T09:00:00.000Z', thumbnail: 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', @@ -278,25 +277,16 @@ describe('Trend API', () => { viewCount: 0, }, { - id: 3, - author: '박무성', - title: '제목', - path: 'https://asn6878.tistory.com/10', + id: 1, + author: '안성윤', + title: + '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', + path: 'https://asn6878.tistory.com/9', createdAt: '2022-09-05T09:00:00.000Z', 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/11', - createdAt: '2022-09-05T10:00:00.000Z', - thumbnail: - 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', - viewCount: 0, - }, ], }); }); @@ -316,27 +306,15 @@ describe('Trend API', () => { message: '트렌드 피드 조회 완료', data: [ { - id: 1, - author: '안성윤', - title: - '자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)', - path: 'https://asn6878.tistory.com/9', - createdAt: '2022-09-05T09:00:00.000Z', + id: 4, + author: '박무성', + title: '제목', + path: 'https://asn6878.tistory.com/11', + createdAt: '2022-09-05T10:00:00.000Z', 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-14T14:07:49.000Z', - thumbnail: - 'https://velog.velcdn.com/images/seok3765/post/2f863481-b594-46f8-9a28-7799afb58aa4/image.jpg', - viewCount: 0, - }, { id: 3, author: '박무성', @@ -348,13 +326,14 @@ describe('Trend API', () => { viewCount: 0, }, { - id: 4, - author: '박무성', - title: '제목', - path: 'https://asn6878.tistory.com/11', - createdAt: '2022-09-05T10:00:00.000Z', + 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-14T14:07:49.000Z', thumbnail: - 'https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png', + 'https://velog.velcdn.com/images/seok3765/post/2f863481-b594-46f8-9a28-7799afb58aa4/image.jpg', viewCount: 0, }, ],