diff --git a/app/src/page/home/user/home.user.resolver.ts b/app/src/page/home/user/home.user.resolver.ts index cc751040..b833f0f1 100644 --- a/app/src/page/home/user/home.user.resolver.ts +++ b/app/src/page/home/user/home.user.resolver.ts @@ -1,12 +1,15 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { StatAuthGuard } from 'src/auth/statAuthGuard'; +import { CacheUtilService } from 'src/cache/cache.util.service'; import { Rate } from 'src/common/models/common.rate.model'; import { UserRank } from 'src/common/models/common.user.model'; import { IntRecord } from 'src/common/models/common.valueRecord.model'; +import { DateWrapper } from 'src/dateWrapper/dateWrapper'; import { HttpExceptionFilter } from 'src/http-exception.filter'; import { HomeUserService } from './home.user.service'; import { + GetHomeUserBlackholedCountRecordsArgs, HomeUser, IntPerCircle, UserCountPerLevel, @@ -16,7 +19,10 @@ import { @UseGuards(StatAuthGuard) @Resolver((_of: unknown) => HomeUser) export class HomeUserResolver { - constructor(private readonly homeUserService: HomeUserService) {} + constructor( + private readonly homeUserService: HomeUserService, + private readonly cacheUtilService: CacheUtilService, + ) {} @Query((_of) => HomeUser) async getHomeUser() { @@ -43,13 +49,32 @@ export class HomeUserResolver { return await this.homeUserService.blackholedRate(); } - @ResolveField((_returns) => [IntRecord], { description: '1 ~ 24 개월' }) + @ResolveField((_returns) => [IntRecord], { + description: '1 ~ 120 개월', + }) async blackholedCountRecords( - @Args('last') last: number, + @Args() { last }: GetHomeUserBlackholedCountRecordsArgs, ): Promise { - return await this.homeUserService.blackholedCountRecords( - Math.max(1, Math.min(last, 24)), - ); + const nextMonth = DateWrapper.nextMonth().toDate(); + const start = DateWrapper.currMonth() + .moveMonth(1 - last) + .toDate(); + + const cacheKey = `homeUserBlackholedCountRecords:${start.getTime()}:${nextMonth.getTime()}`; + + const cached = await this.cacheUtilService.get(cacheKey); + if (cached) { + return cached; + } + + const result = await this.homeUserService.blackholedCountRecords({ + start, + end: nextMonth, + }); + + await this.cacheUtilService.set(cacheKey, result); + + return result; } @ResolveField((_returns) => [IntPerCircle]) diff --git a/app/src/page/home/user/home.user.service.ts b/app/src/page/home/user/home.user.service.ts index 0f9be23c..74dbdd7a 100644 --- a/app/src/page/home/user/home.user.service.ts +++ b/app/src/page/home/user/home.user.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; import { CursusUserCacheService, USER_CORRECTION_POINT_RANKING, @@ -16,7 +17,7 @@ import { assertExist } from 'src/common/assertExist'; import type { Rate } from 'src/common/models/common.rate.model'; import type { UserRank } from 'src/common/models/common.user.model'; import type { IntRecord } from 'src/common/models/common.valueRecord.model'; -import { DateRangeService } from 'src/dateRange/dateRange.service'; +import { RUNTIME_CONFIG } from 'src/config/runtime'; import type { DateRange } from 'src/dateRange/dtos/dateRange.dto'; import { DateWrapper } from 'src/dateWrapper/dateWrapper'; import type { IntPerCircle, UserCountPerLevel } from './models/home.user.model'; @@ -27,7 +28,8 @@ export class HomeUserService { private readonly cursusUserService: CursusUserService, private readonly cursusUserCacheService: CursusUserCacheService, private readonly questsUserService: QuestsUserService, - private readonly dateRangeService: DateRangeService, + @Inject(RUNTIME_CONFIG.KEY) + private readonly runtimeConfig: ConfigType, ) {} @CacheOnReturn() @@ -90,46 +92,39 @@ export class HomeUserService { }; } - @CacheOnReturn() - async blackholedCountRecords(last: number): Promise { - const startDate = new DateWrapper() - .startOfMonth() - .moveMonth(1 - last) - .toDate(); - - const blackholeds: { blackholedAt?: Date }[] = - await this.cursusUserService.findAllAndLean({ - filter: blackholedUserFilterByDateRange({ - start: startDate, - end: new Date(), - }), - select: { blackholedAt: 1 }, + async blackholedCountRecords({ + start, + end, + }: DateRange): Promise { + return await this.cursusUserService + .aggregate() + .match(blackholedUserFilterByDateRange({ start, end })) + .group({ + _id: { + $dateFromParts: { + year: { + $year: { + date: '$blackholedAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + month: { + $month: { + date: '$blackholedAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + count: { $count: {} }, + }) + .sort({ _id: 1 }) + .project({ + _id: 0, + at: '$_id', + value: '$count', }); - - const res = blackholeds.reduce((acc, { blackholedAt }) => { - assertExist(blackholedAt); - - const date = new DateWrapper(blackholedAt) - .startOfMonth() - .toDate() - .getTime(); - - const prev = acc.get(date); - - acc.set(date, (prev ?? 0) + 1); - - return acc; - }, new Map() as Map); - - const records: IntRecord[] = []; - - for (let i = 0; i < last; i++) { - const currDate = new DateWrapper(startDate).moveMonth(i).toDate(); - - records.push({ at: currDate, value: res.get(currDate.getTime()) ?? 0 }); - } - - return records; } @CacheOnReturn() diff --git a/app/src/page/home/user/models/home.user.model.ts b/app/src/page/home/user/models/home.user.model.ts index 3e34c901..e0b6fd9d 100644 --- a/app/src/page/home/user/models/home.user.model.ts +++ b/app/src/page/home/user/models/home.user.model.ts @@ -1,4 +1,5 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { ArgsType, Field, ObjectType } from '@nestjs/graphql'; +import { Max, Min } from 'class-validator'; import { Rate } from 'src/common/models/common.rate.model'; import { UserRank } from 'src/common/models/common.user.model'; import { IntRecord } from 'src/common/models/common.valueRecord.model'; @@ -35,7 +36,7 @@ export class HomeUser { @Field() blackholedRate: Rate; - @Field((_type) => [IntRecord]) + @Field((_type) => [IntRecord], { description: '1 ~ 120 개월' }) blackholedCountRecords: IntRecord[]; @Field((_type) => [IntPerCircle]) @@ -50,3 +51,11 @@ export class HomeUser { @Field((_type) => [IntPerCircle]) averageDurationPerCircle: IntPerCircle[]; } + +@ArgsType() +export class GetHomeUserBlackholedCountRecordsArgs { + @Min(1) + @Max(120) + @Field() + last: number; +} diff --git a/app/src/schema.gql b/app/src/schema.gql index 9e807e50..e5486e4a 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -244,6 +244,8 @@ type HomeUser { userCountPerLevel: [UserCountPerLevel!]! memberRate: Rate! blackholedRate: Rate! + + """1 ~ 120 개월""" blackholedCountRecords(last: Int!): [IntRecord!]! blackholedCountPerCircle: [IntPerCircle!]! walletRanking(limit: Int! = 5): [UserRank!]!