From 707ddfac807838776df65636b88a1bd8004b4ad7 Mon Sep 17 00:00:00 2001 From: Yeongbin Im <56269396+yeongbinim@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:52:45 +0900 Subject: [PATCH] =?UTF-8?q?[BE#374]=20polling=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8B=9C=EA=B0=84=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=8B=B1=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 + BE/src/common/redis.service.ts | 18 ++++-- BE/src/heartbeat/heartbeat.controller.ts | 16 +++++ BE/src/heartbeat/heartbeat.module.ts | 13 ++++ BE/src/heartbeat/heartbeat.service.ts | 27 ++++++++ BE/src/main.ts | 4 ++ BE/src/mates/mates.service.ts | 7 ++- .../dto/request/create-study-logs.dto.ts | 4 +- BE/src/study-logs/study-logs.controller.ts | 6 +- BE/src/study-logs/study-logs.service.ts | 62 ++++++++++++++++--- 10 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 BE/src/heartbeat/heartbeat.controller.ts create mode 100644 BE/src/heartbeat/heartbeat.module.ts create mode 100644 BE/src/heartbeat/heartbeat.service.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 667a385..67e0317 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -14,6 +14,7 @@ import { LoggingMiddleware } from './common/middleware/logging.middleware'; import { typeormConfig } from './common/config/typeorm.config'; import { staticConfig } from './common/config/static.config'; import { AdminModule } from './admin/admin.module'; +import { HeartbeatModule } from './heartbeat/heartbeat.module'; @Module({ imports: [ @@ -32,6 +33,7 @@ import { AdminModule } from './admin/admin.module'; PassportModule, AuthModule, AdminModule, + HeartbeatModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/common/redis.service.ts b/BE/src/common/redis.service.ts index 1423c3f..0e58d34 100644 --- a/BE/src/common/redis.service.ts +++ b/BE/src/common/redis.service.ts @@ -8,15 +8,23 @@ export class RedisService { this.client = createClient(); this.client.connect(); } - async set(key: string, value: string) { - await this.client.set(key, value); + async hset(key: string, field: string, value: string) { + await this.client.hSet(key, field, value); } - get(key: string): Promise { - return this.client.get(key); + hget(key: string, field: string): Promise { + return this.client.hGet(key, field); } - async del(key: string): Promise { + async hdel(key: string, field: string): Promise { + await this.client.hDel(key, field); + } + + async del(key: string) { await this.client.del(key); } + + getKeys() { + return this.client.keys('*'); + } } diff --git a/BE/src/heartbeat/heartbeat.controller.ts b/BE/src/heartbeat/heartbeat.controller.ts new file mode 100644 index 0000000..6791486 --- /dev/null +++ b/BE/src/heartbeat/heartbeat.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { HeartbeatService } from './heartbeat.service'; +import { User } from 'src/users/decorator/user.decorator'; +import { AccessTokenGuard } from 'src/auth/guard/bearer-token.guard'; + +@Controller() +export class HeartbeatController { + constructor(private readonly heartbeatsService: HeartbeatService) {} + + @UseGuards(AccessTokenGuard) + @Get('/heartbeat') + heartbeat(@User('id') userId: number) { + this.heartbeatsService.recordHeartbeat(userId); + return { statusCode: 200, message: 'OK' }; + } +} diff --git a/BE/src/heartbeat/heartbeat.module.ts b/BE/src/heartbeat/heartbeat.module.ts new file mode 100644 index 0000000..aba62a7 --- /dev/null +++ b/BE/src/heartbeat/heartbeat.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { HeartbeatService } from './heartbeat.service'; +import { HeartbeatController } from './heartbeat.controller'; +import { AuthModule } from 'src/auth/auth.module'; +import { UsersModule } from 'src/users/users.module'; +import { RedisService } from 'src/common/redis.service'; + +@Module({ + imports: [AuthModule, UsersModule], + controllers: [HeartbeatController], + providers: [HeartbeatService, RedisService], +}) +export class HeartbeatModule {} diff --git a/BE/src/heartbeat/heartbeat.service.ts b/BE/src/heartbeat/heartbeat.service.ts new file mode 100644 index 0000000..a49f267 --- /dev/null +++ b/BE/src/heartbeat/heartbeat.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { RedisService } from 'src/common/redis.service'; + +@Injectable() +export class HeartbeatService { + constructor(private redisService: RedisService) {} + + recordHeartbeat(userId: number) { + this.redisService.hset(`${userId}`, 'received_at', `${Date.now()}`); + } + + async startCheckingHeartbeats() { + setInterval(async () => { + const now = Date.now(); + const clients = await this.redisService.getKeys(); + for (const clientId of clients) { + const received_at = await this.redisService.hget( + `${clientId}`, + 'received_at', + ); + if (now - +received_at > 30000) { + await this.redisService.del(`${clientId}`); + } + } + }, 10000); + } +} diff --git a/BE/src/main.ts b/BE/src/main.ts index 6cf92fb..25d02a0 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -10,6 +10,7 @@ import { LoggingInterceptor } from './common/interceptor/logging.interceptor'; import { HttpExceptionFilter } from './common/exception-filter/http-exception-filter'; import { swaggerConfig } from './common/config/swagger.config'; import { ENV } from './common/const/env-keys.const'; +import { HeartbeatService } from './heartbeat/heartbeat.service'; async function bootstrap() { const configService = new ConfigService(); @@ -35,6 +36,9 @@ async function bootstrap() { app.useGlobalInterceptors(new LoggingInterceptor()); app.useGlobalFilters(new HttpExceptionFilter()); + const heartbeatService = app.get(HeartbeatService); + heartbeatService.startCheckingHeartbeats(); + await app.listen(configService.get(ENV.PORT) || 3000); } bootstrap(); diff --git a/BE/src/mates/mates.service.ts b/BE/src/mates/mates.service.ts index e04a7f0..e43dbd9 100644 --- a/BE/src/mates/mates.service.ts +++ b/BE/src/mates/mates.service.ts @@ -68,7 +68,10 @@ export class MatesService { ); return Promise.all( studyTimeByFollowing.map(async (record) => { - const started_at = await this.redisService.get(`${record.id}`); + const started_at = await this.redisService.hget( + `${record.id}`, + 'started_at', + ); return { ...record, image_url: getImageUrl( @@ -89,7 +92,7 @@ export class MatesService { const userIds = result.map((following) => following.following_id.id); return Promise.all( userIds.map(async (id) => { - const started_at = await this.redisService.get(`${id}`); + const started_at = await this.redisService.hget(`${id}`, 'started_at'); return { id, started_at }; }), ); diff --git a/BE/src/study-logs/dto/request/create-study-logs.dto.ts b/BE/src/study-logs/dto/request/create-study-logs.dto.ts index 3fb8b20..aede1a2 100644 --- a/BE/src/study-logs/dto/request/create-study-logs.dto.ts +++ b/BE/src/study-logs/dto/request/create-study-logs.dto.ts @@ -5,14 +5,14 @@ export class StudyLogsCreateDto { example: '2023-11-23', description: '학습을 시작한 날짜', }) - date: Date; + date: string; @ApiProperty({ type: 'date', example: '2023-11-23 11:00:12', description: '학습을 시작/종료 시점의 시간', }) - created_at: Date; + created_at: string; @ApiProperty({ type: 'enum', diff --git a/BE/src/study-logs/study-logs.controller.ts b/BE/src/study-logs/study-logs.controller.ts index 1616481..9750606 100644 --- a/BE/src/study-logs/study-logs.controller.ts +++ b/BE/src/study-logs/study-logs.controller.ts @@ -38,15 +38,11 @@ export class StudyLogsController { @User('id') userId: number, @Body() studyLogsData: StudyLogsCreateDto, ): Promise { - const { created_at, learning_time, type } = studyLogsData; + const { type } = studyLogsData; if (type === 'start') { await this.studyLogsService.createStartLog(studyLogsData, userId); return new ResponseDto(200, 'OK'); } - studyLogsData.date = this.studyLogsService.calculateStartDay( - new Date(created_at), - learning_time, - ); await this.studyLogsService.createFinishLog(studyLogsData, userId); return new ResponseDto(200, 'OK'); } diff --git a/BE/src/study-logs/study-logs.service.ts b/BE/src/study-logs/study-logs.service.ts index 08994d1..6325f12 100644 --- a/BE/src/study-logs/study-logs.service.ts +++ b/BE/src/study-logs/study-logs.service.ts @@ -25,25 +25,35 @@ export class StudyLogsService { user_id: number, ): Promise { const { created_at } = studyLogsData; - await this.redisService.set(`${user_id}`, `${created_at}`); + await this.redisService.hset(`${user_id}`, 'started_at', `${created_at}`); + await this.redisService.hset(`${user_id}`, 'received_at', `${Date.now()}`); } async createFinishLog( studyLogsData: StudyLogsCreateDto, user_id: number, - ): Promise { - const { category_id, ...data } = studyLogsData; + ): Promise { + const { category_id } = studyLogsData; const user = { id: user_id } as UsersModel; const category = { id: category_id ?? null } as Categories; - const studyLog = this.studyLogsRepository.create({ - ...data, - user_id: user, - category_id: category, - }); - const savedStudyLog = await this.studyLogsRepository.save(studyLog); + const learningTimes = this.calculateLearningTimes( + studyLogsData.created_at, + studyLogsData.learning_time, + ); + + for (const { started_at, date, learning_time } of learningTimes) { + const studyLog = this.studyLogsRepository.create({ + type: 'finish', + date, + learning_time, + created_at: started_at, + user_id: user, + category_id: category, + }); + await this.studyLogsRepository.save(studyLog); + } await this.redisService.del(`${user_id}`); - return this.entityToDto(savedStudyLog); } async findAll(): Promise { @@ -59,6 +69,38 @@ export class StudyLogsService { return started_at; } + calculateLearningTimes( + created_at: string, + learning_time: number, + ): { started_at: string; date: string; learning_time: number }[] { + const finishedAt = moment(new Date(created_at)); + const startedAt = finishedAt.clone().subtract(learning_time, 's'); + if (startedAt.get('date') !== finishedAt.get('date')) { + return [ + { + started_at: startedAt.toISOString(), + date: startedAt.format('YYYY-MM-DD'), + learning_time: + startedAt.clone().endOf('day').diff(startedAt, 's') + 1, + }, + { + started_at: finishedAt.clone().startOf('day').toISOString(), + date: finishedAt.format('YYYY-MM-DD'), + learning_time: finishedAt + .clone() + .diff(finishedAt.clone().startOf('day'), 's'), + }, + ]; + } + return [ + { + started_at: startedAt.toISOString(), + date: startedAt.format('YYYY-MM-DD'), + learning_time: learning_time, + }, + ]; + } + async calculateTotalTimes( user_id: number, start_date: string,