From 5586db22b23411a410599fea13366d1daa15dbce Mon Sep 17 00:00:00 2001 From: jaham Date: Wed, 15 Nov 2023 01:05:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20dailyAliveUserCountRecords?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - close #392 --- app/src/page/home/user/home.user.resolver.ts | 28 ++++ app/src/page/home/user/home.user.service.ts | 146 +++++++++++++++++- .../page/home/user/models/home.user.model.ts | 13 ++ app/src/schema.gql | 3 +- 4 files changed, 188 insertions(+), 2 deletions(-) diff --git a/app/src/page/home/user/home.user.resolver.ts b/app/src/page/home/user/home.user.resolver.ts index b833f0f1..1cd48808 100644 --- a/app/src/page/home/user/home.user.resolver.ts +++ b/app/src/page/home/user/home.user.resolver.ts @@ -9,6 +9,7 @@ import { DateWrapper } from 'src/dateWrapper/dateWrapper'; import { HttpExceptionFilter } from 'src/http-exception.filter'; import { HomeUserService } from './home.user.service'; import { + GetHomeUserAliveUserCountRecordsArgs, GetHomeUserBlackholedCountRecordsArgs, HomeUser, IntPerCircle, @@ -29,6 +30,33 @@ export class HomeUserResolver { return {}; } + @ResolveField((_returns) => [IntRecord]) + async dailyAliveUserCountRecords( + @Args() { last }: GetHomeUserAliveUserCountRecordsArgs, + ): Promise { + const nextDay = new DateWrapper().startOfDate().moveDate(1).toDate(); + const start = new DateWrapper() + .startOfDate() + .moveDate(1 - last) + .toDate(); + + const cacheKey = `homeUserDailyAliveUserCountRecords:${start.getTime()}:${nextDay.getTime()}`; + + const cached = await this.cacheUtilService.get(cacheKey); + if (cached) { + return cached; + } + + const result = await this.homeUserService.dailyAliveUserCountRecords({ + start, + end: nextDay, + }); + + await this.cacheUtilService.set(cacheKey, result, DateWrapper.MIN); + + return result; + } + @ResolveField((_returns) => [IntRecord]) async aliveUserCountRecords(): Promise { return await this.homeUserService.aliveUserCountRecords(); diff --git a/app/src/page/home/user/home.user.service.ts b/app/src/page/home/user/home.user.service.ts index 74dbdd7a..bd5132a2 100644 --- a/app/src/page/home/user/home.user.service.ts +++ b/app/src/page/home/user/home.user.service.ts @@ -11,9 +11,9 @@ import { blackholedUserFilterByDateRange, } from 'src/api/cursusUser/db/cursusUser.database.query'; import type { cursus_user } from 'src/api/cursusUser/db/cursusUser.database.schema'; +import { promo } from 'src/api/promo/db/promo.database.schema'; import { QuestsUserService } from 'src/api/questsUser/questsUser.service'; import { CacheOnReturn } from 'src/cache/decrators/onReturn/cache.decorator.onReturn.symbol'; -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'; @@ -32,6 +32,150 @@ export class HomeUserService { private readonly runtimeConfig: ConfigType, ) {} + // todo: 나중에 별도 service 로 분리해야 합니다. + async dailyAliveUserCountRecords({ + start, + end, + }: DateRange): Promise { + const totalUserIOs = await this.cursusUserService + .aggregate<{ date: Date; count: number }>() + .match( + blackholedUserFilterByDateRange({ + start: new Date(0), + end, + }), + ) + .group({ + _id: { + $dateFromParts: { + year: { + $year: { + date: '$blackholedAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + month: { + $month: { + date: '$blackholedAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + day: { + $dayOfMonth: { + date: '$blackholedAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + count: { $sum: -1 }, + }) + .project({ + _id: 0, + date: '$_id', + count: 1, + }) + .unionWith({ + // todo: collection 분리 + coll: 'transfer_ins', + pipeline: [ + { + $match: { + date: { + $gte: start, + $lt: end, + }, + }, + }, + { + $group: { + _id: '$date', + count: { $count: {} }, + }, + }, + { + $project: { + _id: 0, + date: '$_id', + count: 1, + }, + }, + ], + }) + .unionWith({ + coll: `${promo.name}s`, + pipeline: [ + { + $match: { + beginAt: { + $lt: end, + }, + }, + }, + { + $project: { + _id: 0, + date: { + $dateFromParts: { + year: { + $year: { + date: '$beginAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + month: { + $month: { + date: '$beginAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + day: { + $dayOfMonth: { + date: '$beginAt', + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + timezone: this.runtimeConfig.TIMEZONE, + }, + }, + count: '$userCount', + }, + }, + ], + }) + .group({ + _id: '$date', + count: { $sum: '$count' }, + }) + .sort({ _id: 1 }) + .project({ + _id: 0, + date: '$_id', + count: 1, + }); + + const startDateIndex = totalUserIOs.findIndex(({ date }) => date >= start); + const beforeStart = totalUserIOs + .slice(0, startDateIndex) + .reduce((acc, { count }) => acc + count, 0); + + return totalUserIOs.slice(startDateIndex).reduce( + ({ prev, records }, { date, count }) => { + records.push({ + at: date, + value: prev + count, + }); + + return { + prev: prev + count, + records, + }; + }, + { prev: beforeStart, records: new Array() }, + ).records; + } + @CacheOnReturn() async aliveUserCountRecords(): Promise { const now = new DateWrapper(); 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 e0b6fd9d..5810f010 100644 --- a/app/src/page/home/user/models/home.user.model.ts +++ b/app/src/page/home/user/models/home.user.model.ts @@ -24,7 +24,12 @@ export class UserCountPerLevel { @ObjectType() export class HomeUser { + // todo: api version header 를 받는 등의 방식을 사용하면 기존 field 명으로 처리할 수 있습니다만, + // 프론트엔드가 현재 작업을 할 수 없는 상황이기 때문에 이렇게 처리합니다. @Field((_type) => [IntRecord]) + dailyAliveUserCountRecords: IntRecord[]; + + @Field((_type) => [IntRecord], { deprecationReason: 'v0.10.0' }) aliveUserCountRecords: IntRecord[]; @Field((_type) => [UserCountPerLevel]) @@ -52,6 +57,14 @@ export class HomeUser { averageDurationPerCircle: IntPerCircle[]; } +@ArgsType() +export class GetHomeUserAliveUserCountRecordsArgs { + @Min(1) + @Max(750) + @Field() + last: number; +} + @ArgsType() export class GetHomeUserBlackholedCountRecordsArgs { @Min(1) diff --git a/app/src/schema.gql b/app/src/schema.gql index e5486e4a..072598a7 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -240,7 +240,8 @@ type UserCountPerLevel { } type HomeUser { - aliveUserCountRecords: [IntRecord!]! + dailyAliveUserCountRecords(last: Int!): [IntRecord!]! + aliveUserCountRecords: [IntRecord!]! @deprecated(reason: "v0.10.0") userCountPerLevel: [UserCountPerLevel!]! memberRate: Rate! blackholedRate: Rate!