From 6c790a56b863156e7719c8fc3e61e33e020b0bf5 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Sun, 3 Mar 2024 17:24:33 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20:sparkles:=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AC=EC=9A=A9=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20temp=5Ffollows=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/follow/db/follow.database.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/follow/db/follow.database.schema.ts b/app/src/follow/db/follow.database.schema.ts index d3a010b9..be97a7d8 100644 --- a/app/src/follow/db/follow.database.schema.ts +++ b/app/src/follow/db/follow.database.schema.ts @@ -3,7 +3,7 @@ import { HydratedDocument } from 'mongoose'; export type UserDocument = HydratedDocument; -@Schema({ collection: 'follows' }) +@Schema({ collection: 'temp_follows' }) export class follow { @Prop({ required: true }) userId: number; From f0cb0d3a8e442d1a0532c5edc17462c89c5ccf48 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Sun, 3 Mar 2024 17:26:59 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20:sparkles:=20feed=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=9C=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/app.module.ts | 2 + app/src/feed/db/feed.database.schema.ts | 21 +++ app/src/feed/dto/feed.dto.ts | 13 ++ app/src/feed/feed.cache.service.ts | 0 app/src/feed/feed.module.ts | 18 +++ app/src/feed/feed.resolver.ts | 29 ++++ app/src/feed/feed.service.ts | 30 ++++ app/src/feed/model/feed.model.ts | 182 ++++++++++++++++++++++++ app/src/schema.gql | 19 +++ 9 files changed, 314 insertions(+) create mode 100644 app/src/feed/db/feed.database.schema.ts create mode 100644 app/src/feed/dto/feed.dto.ts create mode 100644 app/src/feed/feed.cache.service.ts create mode 100644 app/src/feed/feed.module.ts create mode 100644 app/src/feed/feed.resolver.ts create mode 100644 app/src/feed/feed.service.ts create mode 100644 app/src/feed/model/feed.model.ts diff --git a/app/src/app.module.ts b/app/src/app.module.ts index bdc88199..f83031d4 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -19,6 +19,7 @@ import { JWT_CONFIG } from './config/jwt'; import { RUNTIME_CONFIG } from './config/runtime'; import { MongooseRootModule } from './database/mongoose/database.mongoose.module'; import { DateWrapper } from './dateWrapper/dateWrapper'; +import { FeedModule } from './feed/feed.module'; import { FollowModule } from './follow/follow.module'; import { HealthCheckModule } from './healthcheck/healthcheck.module'; import { LambdaModule } from './lambda/lambda.module'; @@ -107,6 +108,7 @@ export class AppRootModule {} SettingModule, CalculatorModule, FollowModule, + FeedModule, LambdaModule, HealthCheckModule, CacheDecoratorOnReturnModule, diff --git a/app/src/feed/db/feed.database.schema.ts b/app/src/feed/db/feed.database.schema.ts new file mode 100644 index 00000000..b5ae1e86 --- /dev/null +++ b/app/src/feed/db/feed.database.schema.ts @@ -0,0 +1,21 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument } from 'mongoose'; + +export type FeedDocument = HydratedDocument; + +@Schema({ collection: 'feeds' }) +export class feed { + @Prop({ required: true }) + id: number; + + @Prop({ required: true }) + userId: number; + + @Prop({ required: true }) + type: string; + + @Prop({ required: true }) + feedAt: Date; +} + +export const FeedSchema = SchemaFactory.createForClass(feed); diff --git a/app/src/feed/dto/feed.dto.ts b/app/src/feed/dto/feed.dto.ts new file mode 100644 index 00000000..8615fdd1 --- /dev/null +++ b/app/src/feed/dto/feed.dto.ts @@ -0,0 +1,13 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum FeedType { + FOLLOW = 'FOLLOW', + STATUS_MESSAGE = 'STATUS_MESSAGE', + LOCATION = 'LOCATION', + NEW_MEMBER = 'NEW_MEMBER', + BLACKHOLED_AT = 'BLACKHOLED_AT', + TEAM_STATUS_FINISHED = 'TEAM_STATUS_FINISHED', + EVENT = 'EVENT', +} + +registerEnumType(FeedType, { name: 'FeedType' }); diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts new file mode 100644 index 00000000..7b142434 --- /dev/null +++ b/app/src/feed/feed.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { FollowModule } from 'src/follow/follow.module'; +import { FeedSchema, feed } from './db/feed.database.schema'; +import { FeedResolver } from './feed.resolver'; +import { FeedService } from './feed.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), + FollowModule, + ], + providers: [FeedResolver, FeedService], //, FeedCacheService], + exports: [], +}) + +// eslint-disable-next-line +export class FeedModule {} diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts new file mode 100644 index 00000000..a78a36b6 --- /dev/null +++ b/app/src/feed/feed.resolver.ts @@ -0,0 +1,29 @@ +import { UseGuards } from '@nestjs/common'; +import { Mutation, Query, Resolver } from '@nestjs/graphql'; +import { MyUserId } from 'src/auth/myContext'; +import { StatAuthGuard } from 'src/auth/statAuthGuard'; +import { FeedType } from './dto/feed.dto'; +import { FeedService } from './feed.service'; +import { Feed } from './model/feed.model'; + +@UseGuards(StatAuthGuard) +@Resolver() +export class FeedResolver { + constructor(private readonly feedService: FeedService) {} + @Query((_returns) => [Feed]) + async getFeed(@MyUserId() userId: number): Promise { + return await this.feedService.getFeed(userId); + } + + @Mutation((_returns) => Boolean) + async updateFeed(@MyUserId() userId: number, feed: Feed): Promise { + await this.feedService.updateFeed(userId, { + id: 1, + userId: 81730, + feedAt: new Date(), + type: FeedType.FOLLOW, + }); + + return true; + } +} diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts new file mode 100644 index 00000000..75024b82 --- /dev/null +++ b/app/src/feed/feed.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { FollowCacheService } from 'src/follow/follow.cache.service'; +import { Feed } from './model/feed.model'; + +@Injectable() +export class FeedService { + constructor(private readonly followCacheService: FollowCacheService) {} + + async getFeed(userId: number): Promise { + //cache에서 feed를 가져옴 + + //없을 시 db에서 가져옴 + return []; + } + + //fanout-on-write 방식 + async updateFeed(userId: number, feed: Feed): Promise { + //나의 팔로워 리스트를 가져옴 + const cachedFollowerList = await this.followCacheService.get( + userId, + 'follower', + ); + + //팔로워들의 feed에 새로 작성한 feed를 추가 + for (const follower of cachedFollowerList) { + console.log('write feed: ', follower.userPreview.id, feed); + //await this.feedCacheService.writeFeed(follower.userPreview.id, feed); + } + } +} diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts new file mode 100644 index 00000000..2e8abc7f --- /dev/null +++ b/app/src/feed/model/feed.model.ts @@ -0,0 +1,182 @@ +import { Field, ObjectType, createUnionType } from '@nestjs/graphql'; +import { UserPreview } from 'src/common/models/common.user.model'; +import { FeedType } from '../dto/feed.dto'; + +@ObjectType() +export class Feed { + @Field() + id: number; + + @Field() + userId: number; + + @Field() + feedAt: Date; + + @Field((_type) => FeedType) + type: FeedType; +} + +@ObjectType() +export class FollowFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field((_type) => UserPreview) + followed: UserPreview; +} + +@ObjectType() +export class StatusMessageFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field() + message: string; +} + +@ObjectType() +export class EventFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field() + event: string; +} + +@ObjectType() +export class BlackholedAtFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field() + blackholedAt: Date; +} + +@ObjectType() +export class TeamStatusFinishedFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field() + teamInfo: string; +} + +@ObjectType() +export class NewMemberFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; +} + +@ObjectType() +export class LocationFeed { + @Field((_type) => FeedType) + type: FeedType; + + @Field() + id: number; + + @Field() + at: Date; + + @Field((_type) => UserPreview) + userPreview: UserPreview; + + @Field() + location: string; +} + +export const FeedUnion = createUnionType({ + name: 'Feed', + types: () => + [ + FollowFeed, + StatusMessageFeed, + EventFeed, + BlackholedAtFeed, + TeamStatusFinishedFeed, + NewMemberFeed, + LocationFeed, + ] as const, + resolveType: ( + value: + | FollowFeed + | StatusMessageFeed + | EventFeed + | BlackholedAtFeed + | TeamStatusFinishedFeed + | NewMemberFeed + | LocationFeed, + ) => { + switch (value.type) { + case FeedType.FOLLOW: + return FollowFeed; + case FeedType.STATUS_MESSAGE: + return StatusMessageFeed; + case FeedType.EVENT: + return EventFeed; + case FeedType.BLACKHOLED_AT: + return BlackholedAtFeed; + case FeedType.TEAM_STATUS_FINISHED: + return TeamStatusFinishedFeed; + case FeedType.NEW_MEMBER: + return NewMemberFeed; + case FeedType.LOCATION: + return LocationFeed; + } + }, +}); diff --git a/app/src/schema.gql b/app/src/schema.gql index 9149aafc..96335fcd 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -77,6 +77,23 @@ type FollowSuccess { followId: Int! } +type Feed { + id: Int! + userId: Int! + feedAt: DateTime! + type: FeedType! +} + +enum FeedType { + FOLLOW + STATUS_MESSAGE + LOCATION + NEW_MEMBER + BLACKHOLED_AT + TEAM_STATUS_FINISHED + EVENT +} + type LinkableAccount { platform: String! id: String! @@ -642,6 +659,7 @@ type Query { getIsFollowing(targetId: Int!): Boolean! getFollowerPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! getFollowingPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! + getFeed: [Feed!]! } enum EvalLogSortOrder { @@ -664,6 +682,7 @@ type Mutation { deleteAccount: Int! followUser(targetId: Int!): FollowSuccess! unfollowUser(targetId: Int!): FollowSuccess! + updateFeed: Boolean! } union LoginResult = LoginSuccess | LoginNotLinked From 7ff37db3113759d6818735712cf43c8eae521bf2 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Fri, 8 Mar 2024 16:00:21 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20:recycle:=20feedbase=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A5=98=20(type=EC=9D=80=20=EA=B0=81=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=84=B1=EA=B2=A9=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.cache.service.ts | 23 ++++ app/src/feed/feed.resolver.ts | 24 +++- app/src/feed/feed.service.ts | 6 +- app/src/feed/model/feed.model.ts | 178 +++++++++-------------------- app/src/schema.gql | 87 +++++++++++--- 5 files changed, 168 insertions(+), 150 deletions(-) diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts index e69de29b..e055d1d4 100644 --- a/app/src/feed/feed.cache.service.ts +++ b/app/src/feed/feed.cache.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { FeedUnion } from './model/feed.model'; + +@Injectable() +export class FeedCacheService { + constructor() {} + + private feedCache = new Map(); + + async get(userId: number): Promise<(typeof FeedUnion)[]> { + return this.feedCache.get(userId) || []; + } + + async set(userId: number, feed: (typeof FeedUnion)[]): Promise { + this.feedCache.set(userId, feed); + } + + async writeFeed(userId: number, feed: typeof FeedUnion): Promise { + const cachedFeedList = await this.get(userId); + cachedFeedList.push(feed); + await this.set(userId, cachedFeedList); + } +} diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index a78a36b6..dd53058d 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -4,24 +4,36 @@ import { MyUserId } from 'src/auth/myContext'; import { StatAuthGuard } from 'src/auth/statAuthGuard'; import { FeedType } from './dto/feed.dto'; import { FeedService } from './feed.service'; -import { Feed } from './model/feed.model'; +import { FeedUnion } from './model/feed.model'; @UseGuards(StatAuthGuard) @Resolver() export class FeedResolver { constructor(private readonly feedService: FeedService) {} - @Query((_returns) => [Feed]) - async getFeed(@MyUserId() userId: number): Promise { + @Query((_returns) => [FeedUnion]) + async getFeed(@MyUserId() userId: number): Promise<(typeof FeedUnion)[]> { return await this.feedService.getFeed(userId); } @Mutation((_returns) => Boolean) - async updateFeed(@MyUserId() userId: number, feed: Feed): Promise { + async updateFeed( + @MyUserId() userId: number, + feed: typeof FeedUnion, + ): Promise { await this.feedService.updateFeed(userId, { id: 1, - userId: 81730, - feedAt: new Date(), + userPreview: { + id: 12345, + login: 'test', + imgUrl: 'testimg', + }, + at: new Date(), type: FeedType.FOLLOW, + followed: { + id: 54321, + login: 'test2', + imgUrl: 'testimg2', + }, }); return true; diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 75024b82..652766e0 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { FollowCacheService } from 'src/follow/follow.cache.service'; -import { Feed } from './model/feed.model'; +import { FeedUnion } from './model/feed.model'; @Injectable() export class FeedService { constructor(private readonly followCacheService: FollowCacheService) {} - async getFeed(userId: number): Promise { + async getFeed(userId: number): Promise<(typeof FeedUnion)[]> { //cache에서 feed를 가져옴 //없을 시 db에서 가져옴 @@ -14,7 +14,7 @@ export class FeedService { } //fanout-on-write 방식 - async updateFeed(userId: number, feed: Feed): Promise { + async updateFeed(userId: number, feed: typeof FeedUnion): Promise { //나의 팔로워 리스트를 가져옴 const cachedFollowerList = await this.followCacheService.get( userId, diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts index 2e8abc7f..13d91adc 100644 --- a/app/src/feed/model/feed.model.ts +++ b/app/src/feed/model/feed.model.ts @@ -3,25 +3,7 @@ import { UserPreview } from 'src/common/models/common.user.model'; import { FeedType } from '../dto/feed.dto'; @ObjectType() -export class Feed { - @Field() - id: number; - - @Field() - userId: number; - - @Field() - feedAt: Date; - - @Field((_type) => FeedType) - type: FeedType; -} - -@ObjectType() -export class FollowFeed { - @Field((_type) => FeedType) - type: FeedType; - +export class FeedBase { @Field() id: number; @@ -30,153 +12,103 @@ export class FollowFeed { @Field((_type) => UserPreview) userPreview: UserPreview; - - @Field((_type) => UserPreview) - followed: UserPreview; } @ObjectType() -export class StatusMessageFeed { +export class FollowFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; - - @Field() - id: number; - - @Field() - at: Date; + type: FeedType.FOLLOW; @Field((_type) => UserPreview) - userPreview: UserPreview; - - @Field() - message: string; + followed: UserPreview; } @ObjectType() -export class EventFeed { +export class LocationFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; + type: FeedType.LOCATION; @Field() - id: number; - - @Field() - at: Date; - - @Field((_type) => UserPreview) - userPreview: UserPreview; - - @Field() - event: string; + location: string; } @ObjectType() -export class BlackholedAtFeed { +export class StatusMessageFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; + type: FeedType.STATUS_MESSAGE; @Field() - id: number; - - @Field() - at: Date; - - @Field((_type) => UserPreview) - userPreview: UserPreview; - - @Field() - blackholedAt: Date; + message: string; } @ObjectType() -export class TeamStatusFinishedFeed { +export class TeamStatusFinishedFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; - - @Field() - id: number; - - @Field() - at: Date; - - @Field((_type) => UserPreview) - userPreview: UserPreview; + type: FeedType.TEAM_STATUS_FINISHED; @Field() teamInfo: string; } @ObjectType() -export class NewMemberFeed { +export class EventFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; + type: FeedType.EVENT; @Field() - id: number; - - @Field() - at: Date; - - @Field((_type) => UserPreview) - userPreview: UserPreview; + event: string; } @ObjectType() -export class LocationFeed { +export class NewMemberFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType; - - @Field() - id: number; + type: FeedType.NEW_MEMBER; @Field() - at: Date; + memberAt: Date; +} - @Field((_type) => UserPreview) - userPreview: UserPreview; +@ObjectType() +export class BlackholedAtFeed extends FeedBase { + @Field((_type) => FeedType) + type: FeedType.BLACKHOLED_AT; @Field() - location: string; + blackholedAt: Date; } export const FeedUnion = createUnionType({ - name: 'Feed', - types: () => - [ - FollowFeed, - StatusMessageFeed, - EventFeed, - BlackholedAtFeed, - TeamStatusFinishedFeed, - NewMemberFeed, - LocationFeed, - ] as const, - resolveType: ( - value: - | FollowFeed - | StatusMessageFeed - | EventFeed - | BlackholedAtFeed - | TeamStatusFinishedFeed - | NewMemberFeed - | LocationFeed, - ) => { - switch (value.type) { - case FeedType.FOLLOW: - return FollowFeed; - case FeedType.STATUS_MESSAGE: - return StatusMessageFeed; - case FeedType.EVENT: - return EventFeed; - case FeedType.BLACKHOLED_AT: - return BlackholedAtFeed; - case FeedType.TEAM_STATUS_FINISHED: - return TeamStatusFinishedFeed; - case FeedType.NEW_MEMBER: - return NewMemberFeed; - case FeedType.LOCATION: - return LocationFeed; + name: 'FeedUnion', + types: () => [ + FollowFeed, + LocationFeed, + StatusMessageFeed, + TeamStatusFinishedFeed, + EventFeed, + NewMemberFeed, + BlackholedAtFeed, + ], + resolveType: (value) => { + if ('followed' in value) { + return FollowFeed; + } + if ('location' in value) { + return LocationFeed; + } + if ('message' in value) { + return StatusMessageFeed; + } + if ('teamInfo' in value) { + return TeamStatusFinishedFeed; + } + if ('event' in value) { + return EventFeed; + } + if ('memberAt' in value) { + return NewMemberFeed; + } + if ('blackholedAt' in value) { + return BlackholedAtFeed; } }, }); diff --git a/app/src/schema.gql b/app/src/schema.gql index 96335fcd..3b800ceb 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -77,23 +77,6 @@ type FollowSuccess { followId: Int! } -type Feed { - id: Int! - userId: Int! - feedAt: DateTime! - type: FeedType! -} - -enum FeedType { - FOLLOW - STATUS_MESSAGE - LOCATION - NEW_MEMBER - BLACKHOLED_AT - TEAM_STATUS_FINISHED - EVENT -} - type LinkableAccount { platform: String! id: String! @@ -659,7 +642,7 @@ type Query { getIsFollowing(targetId: Int!): Boolean! getFollowerPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! getFollowingPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! - getFeed: [Feed!]! + getFeed: [FeedUnion!]! } enum EvalLogSortOrder { @@ -672,6 +655,74 @@ enum FollowSortOrder { FOLLOW_AT_DESC } +union FeedUnion = FollowFeed | LocationFeed | StatusMessageFeed | TeamStatusFinishedFeed | EventFeed | NewMemberFeed | BlackholedAtFeed + +type FollowFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + followed: UserPreview! +} + +enum FeedType { + FOLLOW + STATUS_MESSAGE + LOCATION + NEW_MEMBER + BLACKHOLED_AT + TEAM_STATUS_FINISHED + EVENT +} + +type LocationFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + location: String! +} + +type StatusMessageFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + message: String! +} + +type TeamStatusFinishedFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + teamInfo: String! +} + +type EventFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + event: String! +} + +type NewMemberFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + memberAt: DateTime! +} + +type BlackholedAtFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + blackholedAt: DateTime! +} + type Mutation { ftLogin(ftCode: String!): LoginSuccess! googleLogin(google: GoogleLoginInput!, ftCode: String): LoginResult! From 1c93af55a64fc2f6b017497cd3402b6535dc1fee Mon Sep 17 00:00:00 2001 From: niamu01 Date: Fri, 8 Mar 2024 23:09:13 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20:sparkles:=20cursorPagination=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20follow=EA=B4=80=EB=A0=A8=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.module.ts | 3 +- app/src/feed/feed.resolver.ts | 14 ++- app/src/feed/feed.service.ts | 93 +++++++++++++- app/src/feed/model/feed.model.ts | 9 +- app/src/follow/follow.cache.service.ts | 19 +++ app/src/schema.gql | 160 +++++++++++++------------ 6 files changed, 210 insertions(+), 88 deletions(-) diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts index 7b142434..285175ae 100644 --- a/app/src/feed/feed.module.ts +++ b/app/src/feed/feed.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { FollowModule } from 'src/follow/follow.module'; +import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; import { FeedSchema, feed } from './db/feed.database.schema'; import { FeedResolver } from './feed.resolver'; import { FeedService } from './feed.service'; @@ -10,7 +11,7 @@ import { FeedService } from './feed.service'; MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), FollowModule, ], - providers: [FeedResolver, FeedService], //, FeedCacheService], + providers: [FeedResolver, FeedService, PaginationCursorService], //, FeedCacheService], exports: [], }) diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index dd53058d..f6362bd1 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -1,18 +1,22 @@ import { UseGuards } from '@nestjs/common'; -import { Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { MyUserId } from 'src/auth/myContext'; import { StatAuthGuard } from 'src/auth/statAuthGuard'; +import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { FeedType } from './dto/feed.dto'; import { FeedService } from './feed.service'; -import { FeedUnion } from './model/feed.model'; +import { FeedPaginationed, FeedUnion } from './model/feed.model'; @UseGuards(StatAuthGuard) @Resolver() export class FeedResolver { constructor(private readonly feedService: FeedService) {} - @Query((_returns) => [FeedUnion]) - async getFeed(@MyUserId() userId: number): Promise<(typeof FeedUnion)[]> { - return await this.feedService.getFeed(userId); + @Query((_returns) => FeedPaginationed) + async getFeed( + @MyUserId() userId: number, + @Args() args: PaginationCursorArgs, + ): Promise { + return await this.feedService.getFeed({ userId, args }); } @Mutation((_returns) => Boolean) diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 652766e0..11e1d0df 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -1,15 +1,93 @@ import { Injectable } from '@nestjs/common'; import { FollowCacheService } from 'src/follow/follow.cache.service'; -import { FeedUnion } from './model/feed.model'; +import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; +import { + CursorExtractor, + PaginationCursorService, +} from 'src/pagination/cursor/pagination.cursor.service'; +import { FeedType } from './dto/feed.dto'; +import { + EventFeed, + FeedPaginationed, + FeedUnion, + FollowFeed, +} from './model/feed.model'; @Injectable() export class FeedService { - constructor(private readonly followCacheService: FollowCacheService) {} + constructor( + private readonly followCacheService: FollowCacheService, + private readonly paginationCursorService: PaginationCursorService, + ) {} - async getFeed(userId: number): Promise<(typeof FeedUnion)[]> { - //cache에서 feed를 가져옴 + async getFeed({ + userId, + args, + }: { + userId: number; + args: PaginationCursorArgs; + }): Promise { + const followFeeds = await this.getFollowFeeds(userId); + const eventFeeds = await this.getEventFeeds(userId); - //없을 시 db에서 가져옴 + console.log(followFeeds); + + const feeds: (typeof FeedUnion)[] = [...followFeeds, ...eventFeeds]; + + //sort로 정렬 + feeds.sort((a, b) => a.at.getTime() - b.at.getTime()); + + //if (!feeds.length) { + // return this.generateEmptyFeed(); + //} + + //pagination + const totalcount = feeds.length; + const hasNextPage = feeds.length > args.first; + + return this.paginationCursorService.toPaginated( + feeds.slice(0, args.first), + totalcount, + hasNextPage, + cursorExtractor, + ); + } + + //userId의 피드에 뜰 정보 + async getFollowFeeds(userId: number): Promise { + //userId가 팔로우 한 사람들 + const followingList = await this.followCacheService.get( + userId, + 'following', + ); + + const followFeeds: FollowFeed[] = []; + + //followingList가 팔로우한 사람들 + followingList.map(async (follow) => { + const followersFollowing = await this.followCacheService.filterByDate( + follow.userPreview.id, + 'following', + follow.followAt, + ); + + followFeeds.push( + ...followersFollowing.map((follower) => { + return { + id: 1, + at: follow.followAt, + userPreview: follow.userPreview, + type: FeedType.FOLLOW, + followed: follower.userPreview, + }; + }), + ); + }); + + return followFeeds; + } + + async getEventFeeds(userId: number): Promise { return []; } @@ -28,3 +106,8 @@ export class FeedService { } } } + +const cursorExtractor: CursorExtractor = (doc) => { + //todo: cursor 생성 + return doc.id.toString(); +}; diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts index 13d91adc..db65b97b 100644 --- a/app/src/feed/model/feed.model.ts +++ b/app/src/feed/model/feed.model.ts @@ -1,6 +1,7 @@ import { Field, ObjectType, createUnionType } from '@nestjs/graphql'; import { UserPreview } from 'src/common/models/common.user.model'; import { FeedType } from '../dto/feed.dto'; +import { CursorPaginated } from 'src/pagination/cursor/models/pagination.cursor.model'; @ObjectType() export class FeedBase { @@ -17,7 +18,8 @@ export class FeedBase { @ObjectType() export class FollowFeed extends FeedBase { @Field((_type) => FeedType) - type: FeedType.FOLLOW; + //todo: .FOLLOW를 적어줄 수 없음 (ㅜㅜ) + type: FeedType; @Field((_type) => UserPreview) followed: UserPreview; @@ -111,4 +113,7 @@ export const FeedUnion = createUnionType({ return BlackholedAtFeed; } }, -}); +}) as any; //todo: union을 pagination 하는 방법 찾기 + +@ObjectType() +export class FeedPaginationed extends CursorPaginated(FeedUnion) {} diff --git a/app/src/follow/follow.cache.service.ts b/app/src/follow/follow.cache.service.ts index 91b1b8ab..fbc6180f 100644 --- a/app/src/follow/follow.cache.service.ts +++ b/app/src/follow/follow.cache.service.ts @@ -37,4 +37,23 @@ export class FollowCacheService { return cachedData; } + + //todo: cacheUtilService에 추가? + async filterByDate( + userId: number, + type: 'follower' | 'following', + time: Date, + ): Promise { + const key = `${userId}:${type}:${FOLLOW_LISTS}`; + + const cachedData = await this.cacheUtilService.get(key); + + if (!cachedData) { + return []; + } + + return cachedData.filter((follow) => { + return follow.followAt > time; + }); + } } diff --git a/app/src/schema.gql b/app/src/schema.gql index 3b800ceb..d2227892 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -77,6 +77,90 @@ type FollowSuccess { followId: Int! } +type CursorPageInfo { + totalCount: Int! + hasNextPage: Boolean! + endCursor: String +} + +type undefinedEdge { + cursor: String! + node: FeedUnion! +} + +union FeedUnion = FollowFeed | LocationFeed | StatusMessageFeed | TeamStatusFinishedFeed | EventFeed | NewMemberFeed | BlackholedAtFeed + +type FollowFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + followed: UserPreview! +} + +enum FeedType { + FOLLOW + STATUS_MESSAGE + LOCATION + NEW_MEMBER + BLACKHOLED_AT + TEAM_STATUS_FINISHED + EVENT +} + +type LocationFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + location: String! +} + +type StatusMessageFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + message: String! +} + +type TeamStatusFinishedFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + teamInfo: String! +} + +type EventFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + event: String! +} + +type NewMemberFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + memberAt: DateTime! +} + +type BlackholedAtFeed { + id: Int! + at: DateTime! + userPreview: UserPreview! + type: FeedType! + blackholedAt: DateTime! +} + +type FeedPaginationed { + edges: [undefinedEdge!]! + pageInfo: CursorPageInfo! +} + type LinkableAccount { platform: String! id: String! @@ -110,12 +194,6 @@ type ProjectPreview { difficulty: Int } -type CursorPageInfo { - totalCount: Int! - hasNextPage: Boolean! - endCursor: String -} - type TeamPreview { id: Int! name: String! @@ -642,7 +720,7 @@ type Query { getIsFollowing(targetId: Int!): Boolean! getFollowerPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! getFollowingPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! - getFeed: [FeedUnion!]! + getFeed(after: String, first: Int! = 20): FeedPaginationed! } enum EvalLogSortOrder { @@ -655,74 +733,6 @@ enum FollowSortOrder { FOLLOW_AT_DESC } -union FeedUnion = FollowFeed | LocationFeed | StatusMessageFeed | TeamStatusFinishedFeed | EventFeed | NewMemberFeed | BlackholedAtFeed - -type FollowFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - followed: UserPreview! -} - -enum FeedType { - FOLLOW - STATUS_MESSAGE - LOCATION - NEW_MEMBER - BLACKHOLED_AT - TEAM_STATUS_FINISHED - EVENT -} - -type LocationFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - location: String! -} - -type StatusMessageFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - message: String! -} - -type TeamStatusFinishedFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - teamInfo: String! -} - -type EventFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - event: String! -} - -type NewMemberFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - memberAt: DateTime! -} - -type BlackholedAtFeed { - id: Int! - at: DateTime! - userPreview: UserPreview! - type: FeedType! - blackholedAt: DateTime! -} - type Mutation { ftLogin(ftCode: String!): LoginSuccess! googleLogin(google: GoogleLoginInput!, ftCode: String): LoginResult! From ab69a13eefe034609397559064ca76b16499be43 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Sat, 9 Mar 2024 15:43:14 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20:sparkles:=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=EC=97=90=20=EB=A7=9E=EC=97=90=20pagination=20?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84,=20follow=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EB=8B=A4=EB=A5=B8=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EB=93=A4=20mock=20data=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.resolver.ts | 2 +- app/src/feed/feed.service.ts | 174 ++++++++++++++++++++++++++++++---- 2 files changed, 157 insertions(+), 19 deletions(-) diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index f6362bd1..69c71d54 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -16,7 +16,7 @@ export class FeedResolver { @MyUserId() userId: number, @Args() args: PaginationCursorArgs, ): Promise { - return await this.feedService.getFeed({ userId, args }); + return await this.feedService.getFeedPaginated({ userId, args }); } @Mutation((_returns) => Boolean) diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 11e1d0df..dc7b2431 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -7,10 +7,15 @@ import { } from 'src/pagination/cursor/pagination.cursor.service'; import { FeedType } from './dto/feed.dto'; import { + BlackholedAtFeed, EventFeed, FeedPaginationed, FeedUnion, FollowFeed, + LocationFeed, + NewMemberFeed, + StatusMessageFeed, + TeamStatusFinishedFeed, } from './model/feed.model'; @Injectable() @@ -20,35 +25,54 @@ export class FeedService { private readonly paginationCursorService: PaginationCursorService, ) {} - async getFeed({ + async getFeedPaginated({ userId, args, }: { userId: number; args: PaginationCursorArgs; }): Promise { + //pagination을 위해 함수 분리 + //id만 가진 db를 만들어 매번 로컬피드캐시에서 join하기 <- 캐시작업때 고려 const followFeeds = await this.getFollowFeeds(userId); + const locationFeeds = await this.getLocationFeeds(userId); + const statusMessageFeeds = await this.getStatusMessageFeeds(userId); + const teamStatusFinishedFeeds = await this.getTeamStatusFinishedFeeds( + userId, + ); const eventFeeds = await this.getEventFeeds(userId); + const newMemberFeeds = await this.getNewMemberFeeds(userId); + const blackholedAtFeeds = await this.getBlackholedAtFeeds(userId); + + const feeds: (typeof FeedUnion)[] = [ + ...followFeeds, + ...locationFeeds, + ...statusMessageFeeds, + ...teamStatusFinishedFeeds, + ...eventFeeds, + ...newMemberFeeds, + ...blackholedAtFeeds, + ]; + + if (!feeds.length) { + return this.generateEmptyFeed(); + } - console.log(followFeeds); - - const feeds: (typeof FeedUnion)[] = [...followFeeds, ...eventFeeds]; - - //sort로 정렬 - feeds.sort((a, b) => a.at.getTime() - b.at.getTime()); - - //if (!feeds.length) { - // return this.generateEmptyFeed(); - //} + //sort로 정렬 (최신순 고정) + feeds.sort((a, b) => b.at.getTime() - a.at.getTime()); //pagination - const totalcount = feeds.length; - const hasNextPage = feeds.length > args.first; + if (args.after) { + const afterIndex = feeds.findIndex( + (feed) => cursorExtractor(feed) === args.after, + ); + feeds.splice(0, afterIndex + 1); + } return this.paginationCursorService.toPaginated( feeds.slice(0, args.first), - totalcount, - hasNextPage, + feeds.length, + feeds.length > args.first, cursorExtractor, ); } @@ -87,8 +111,114 @@ export class FeedService { return followFeeds; } + async getLocationFeeds(userId: number): Promise { + const locationFeeds: LocationFeed[] = []; + + locationFeeds.push({ + id: 2, + at: new Date(), + userPreview: { + id: 12345, + login: 'nickname123', + imgUrl: 'profileImg123', + }, + type: FeedType.LOCATION, + location: 'c1r1s1', + }); + + return locationFeeds; + } + + async getStatusMessageFeeds(userId: number): Promise { + const statusMessageFeeds: StatusMessageFeed[] = []; + + statusMessageFeeds.push({ + id: 3, + at: new Date(), + userPreview: { + id: 23456, + login: 'nickname234', + imgUrl: 'profileImg234', + }, + type: FeedType.STATUS_MESSAGE, + message: 'status message', + }); + + return statusMessageFeeds; + } + + async getTeamStatusFinishedFeeds( + userId: number, + ): Promise { + const teamStatusFinishedFeeds: TeamStatusFinishedFeed[] = []; + + teamStatusFinishedFeeds.push({ + id: 4, + at: new Date(), + userPreview: { + id: 34567, + login: 'nickname345', + imgUrl: 'profileImg345', + }, + type: FeedType.TEAM_STATUS_FINISHED, + teamInfo: 'team status finished', + }); + + return teamStatusFinishedFeeds; + } + async getEventFeeds(userId: number): Promise { - return []; + const eventFeeds: EventFeed[] = []; + + eventFeeds.push({ + id: 5, + at: new Date(), + userPreview: { + id: 45678, + login: 'nickname456', + imgUrl: 'profileImg456', + }, + type: FeedType.EVENT, + event: 'event', + }); + + return eventFeeds; + } + + async getNewMemberFeeds(userId: number): Promise { + const newMemberFeeds: NewMemberFeed[] = []; + + newMemberFeeds.push({ + id: 6, + at: new Date(), + userPreview: { + id: 56789, + login: 'nickname567', + imgUrl: 'profileImg567', + }, + type: FeedType.NEW_MEMBER, + memberAt: new Date(), + }); + + return newMemberFeeds; + } + + async getBlackholedAtFeeds(userId: number): Promise { + const blackholedAtFeeds: BlackholedAtFeed[] = []; + + blackholedAtFeeds.push({ + id: 7, + at: new Date(), + userPreview: { + id: 67890, + login: 'nickname678', + imgUrl: 'profileImg678', + }, + type: FeedType.BLACKHOLED_AT, + blackholedAt: new Date(), + }); + + return blackholedAtFeeds; } //fanout-on-write 방식 @@ -105,9 +235,17 @@ export class FeedService { //await this.feedCacheService.writeFeed(follower.userPreview.id, feed); } } + + private generateEmptyFeed(): FeedPaginationed { + return this.paginationCursorService.toPaginated( + [], + 0, + false, + cursorExtractor, + ); + } } const cursorExtractor: CursorExtractor = (doc) => { - //todo: cursor 생성 - return doc.id.toString(); + return `${doc.id.toString()} + ${doc.at.toISOString()}`; }; From d1403e8e34fb3afd19a8dd8f448f01807a407f31 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Tue, 12 Mar 2024 23:03:08 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20:bug:=20=EA=B0=92=EC=9D=B4=20?= =?UTF-8?q?0=EC=9D=BC=EB=95=8C=EB=A5=BC=20=EA=B3=A0=EB=A0=A4=ED=95=B4=20nu?= =?UTF-8?q?ll=EC=B2=98=EB=A6=AC=20=EB=B3=80=EA=B2=BD=20||=20->=20=3F=3F,?= =?UTF-8?q?=20push=EC=97=90=EC=84=9C=20=EC=8A=A4=ED=94=84=EB=A0=88?= =?UTF-8?q?=EB=93=9C=EC=97=B0=EC=82=B0=EC=9E=90=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.cache.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts index e055d1d4..9559bcf0 100644 --- a/app/src/feed/feed.cache.service.ts +++ b/app/src/feed/feed.cache.service.ts @@ -8,7 +8,7 @@ export class FeedCacheService { private feedCache = new Map(); async get(userId: number): Promise<(typeof FeedUnion)[]> { - return this.feedCache.get(userId) || []; + return this.feedCache.get(userId) ?? []; } async set(userId: number, feed: (typeof FeedUnion)[]): Promise { @@ -16,8 +16,8 @@ export class FeedCacheService { } async writeFeed(userId: number, feed: typeof FeedUnion): Promise { - const cachedFeedList = await this.get(userId); - cachedFeedList.push(feed); - await this.set(userId, cachedFeedList); + const prev = await this.get(userId); + + await this.set(userId, [...prev, feed]); } } From c4c54fd60a0c1025747e05b523e687162be0fdcd Mon Sep 17 00:00:00 2001 From: niamu01 Date: Tue, 12 Mar 2024 23:06:09 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20:recycle:=20FeedPaginationed?= =?UTF-8?q?=20->=20FeedPaginated,=20db=20=EB=8F=84=ED=81=90=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=82=A0=EC=A7=9C=20createdAt?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3416 --- app/src/feed/db/feed.database.schema.ts | 2 +- app/src/feed/feed.resolver.ts | 6 +++--- app/src/feed/feed.service.ts | 6 +++--- app/src/feed/model/feed.model.ts | 2 +- app/src/follow/db/follow.database.schema.ts | 1 + app/src/schema.gql | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/feed/db/feed.database.schema.ts b/app/src/feed/db/feed.database.schema.ts index b5ae1e86..4b9e3371 100644 --- a/app/src/feed/db/feed.database.schema.ts +++ b/app/src/feed/db/feed.database.schema.ts @@ -15,7 +15,7 @@ export class feed { type: string; @Prop({ required: true }) - feedAt: Date; + createdAt: Date; } export const FeedSchema = SchemaFactory.createForClass(feed); diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index 69c71d54..34de69a9 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -5,17 +5,17 @@ import { StatAuthGuard } from 'src/auth/statAuthGuard'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { FeedType } from './dto/feed.dto'; import { FeedService } from './feed.service'; -import { FeedPaginationed, FeedUnion } from './model/feed.model'; +import { FeedPaginated, FeedUnion } from './model/feed.model'; @UseGuards(StatAuthGuard) @Resolver() export class FeedResolver { constructor(private readonly feedService: FeedService) {} - @Query((_returns) => FeedPaginationed) + @Query((_returns) => FeedPaginated) async getFeed( @MyUserId() userId: number, @Args() args: PaginationCursorArgs, - ): Promise { + ): Promise { return await this.feedService.getFeedPaginated({ userId, args }); } diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index dc7b2431..67db4759 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -9,7 +9,7 @@ import { FeedType } from './dto/feed.dto'; import { BlackholedAtFeed, EventFeed, - FeedPaginationed, + FeedPaginated, FeedUnion, FollowFeed, LocationFeed, @@ -31,7 +31,7 @@ export class FeedService { }: { userId: number; args: PaginationCursorArgs; - }): Promise { + }): Promise { //pagination을 위해 함수 분리 //id만 가진 db를 만들어 매번 로컬피드캐시에서 join하기 <- 캐시작업때 고려 const followFeeds = await this.getFollowFeeds(userId); @@ -236,7 +236,7 @@ export class FeedService { } } - private generateEmptyFeed(): FeedPaginationed { + private generateEmptyFeed(): FeedPaginated { return this.paginationCursorService.toPaginated( [], 0, diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts index db65b97b..b051c4d0 100644 --- a/app/src/feed/model/feed.model.ts +++ b/app/src/feed/model/feed.model.ts @@ -116,4 +116,4 @@ export const FeedUnion = createUnionType({ }) as any; //todo: union을 pagination 하는 방법 찾기 @ObjectType() -export class FeedPaginationed extends CursorPaginated(FeedUnion) {} +export class FeedPaginated extends CursorPaginated(FeedUnion) {} diff --git a/app/src/follow/db/follow.database.schema.ts b/app/src/follow/db/follow.database.schema.ts index be97a7d8..12dd04ed 100644 --- a/app/src/follow/db/follow.database.schema.ts +++ b/app/src/follow/db/follow.database.schema.ts @@ -13,6 +13,7 @@ export class follow { @Prop({ required: true }) followAt: Date; + //createdAt: Date; } export const FollowSchema = SchemaFactory.createForClass(follow); diff --git a/app/src/schema.gql b/app/src/schema.gql index d2227892..aca46b00 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -156,7 +156,7 @@ type BlackholedAtFeed { blackholedAt: DateTime! } -type FeedPaginationed { +type FeedPaginated { edges: [undefinedEdge!]! pageInfo: CursorPageInfo! } @@ -720,7 +720,7 @@ type Query { getIsFollowing(targetId: Int!): Boolean! getFollowerPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! getFollowingPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! - getFeed(after: String, first: Int! = 20): FeedPaginationed! + getFeed(after: String, first: Int! = 20): FeedPaginated! } enum EvalLogSortOrder { From 219e2ef7cffe28b9c8887daae645d12be631f7a1 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Thu, 4 Apr 2024 16:35:03 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20:sparkles:=20follow=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20feed=20db?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/db/feed.database.schema.ts | 21 +- app/src/feed/feed.cache.service.ts | 33 ++-- app/src/feed/feed.module.ts | 4 +- app/src/feed/feed.resolver.ts | 29 +-- app/src/feed/feed.service.ts | 249 +++++------------------- app/src/feed/model/feed.model.ts | 14 +- app/src/follow/follow.module.ts | 18 +- app/src/follow/follow.service.ts | 12 ++ app/src/schema.gql | 58 +++--- 9 files changed, 141 insertions(+), 297 deletions(-) diff --git a/app/src/feed/db/feed.database.schema.ts b/app/src/feed/db/feed.database.schema.ts index 4b9e3371..a4620637 100644 --- a/app/src/feed/db/feed.database.schema.ts +++ b/app/src/feed/db/feed.database.schema.ts @@ -1,21 +1,26 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose'; +import { UserPreview } from 'src/common/models/common.user.model'; +import { FeedType } from '../dto/feed.dto'; export type FeedDocument = HydratedDocument; -@Schema({ collection: 'feeds' }) +@Schema({ collection: 'feeds', discriminatorKey: 'type' }) export class feed { @Prop({ required: true }) - id: number; + createdAt: Date; - @Prop({ required: true }) - userId: number; + @Prop({ required: true, type: UserPreview }) + userPreview: UserPreview; - @Prop({ required: true }) - type: string; + @Prop({ required: true, type: String, enum: Object.values(FeedType) }) + type: FeedType; - @Prop({ required: true }) - createdAt: Date; + @Prop({ type: UserPreview }) + followed?: UserPreview; + + @Prop({ type: String }) + location?: string; } export const FeedSchema = SchemaFactory.createForClass(feed); diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts index 9559bcf0..5014c933 100644 --- a/app/src/feed/feed.cache.service.ts +++ b/app/src/feed/feed.cache.service.ts @@ -1,23 +1,22 @@ -import { Injectable } from '@nestjs/common'; -import { FeedUnion } from './model/feed.model'; +// import { Injectable } from '@nestjs/common'; -@Injectable() -export class FeedCacheService { - constructor() {} +// @Injectable() +// export class FeedCacheService { +// constructor() {} - private feedCache = new Map(); +// private feedCache = new Map(); - async get(userId: number): Promise<(typeof FeedUnion)[]> { - return this.feedCache.get(userId) ?? []; - } +// async get(userId: number): Promise<(typeof FeedUnion)[]> { +// return this.feedCache.get(userId) ?? []; +// } - async set(userId: number, feed: (typeof FeedUnion)[]): Promise { - this.feedCache.set(userId, feed); - } +// async set(userId: number, feed: (typeof FeedUnion)[]): Promise { +// this.feedCache.set(userId, feed); +// } - async writeFeed(userId: number, feed: typeof FeedUnion): Promise { - const prev = await this.get(userId); +// async writeFeed(userId: number, feed: typeof FeedUnion): Promise { +// const prev = await this.get(userId); - await this.set(userId, [...prev, feed]); - } -} +// await this.set(userId, [...prev, feed]); +// } +// } diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts index 285175ae..46f1fa82 100644 --- a/app/src/feed/feed.module.ts +++ b/app/src/feed/feed.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { FollowModule } from 'src/follow/follow.module'; import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; import { FeedSchema, feed } from './db/feed.database.schema'; import { FeedResolver } from './feed.resolver'; @@ -9,10 +8,9 @@ import { FeedService } from './feed.service'; @Module({ imports: [ MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), - FollowModule, ], providers: [FeedResolver, FeedService, PaginationCursorService], //, FeedCacheService], - exports: [], + exports: [FeedService], }) // eslint-disable-next-line diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index 34de69a9..7f84cc4b 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -1,11 +1,10 @@ import { UseGuards } from '@nestjs/common'; -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Query, Resolver } from '@nestjs/graphql'; import { MyUserId } from 'src/auth/myContext'; import { StatAuthGuard } from 'src/auth/statAuthGuard'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; -import { FeedType } from './dto/feed.dto'; import { FeedService } from './feed.service'; -import { FeedPaginated, FeedUnion } from './model/feed.model'; +import { FeedPaginated } from './model/feed.model'; @UseGuards(StatAuthGuard) @Resolver() @@ -18,28 +17,4 @@ export class FeedResolver { ): Promise { return await this.feedService.getFeedPaginated({ userId, args }); } - - @Mutation((_returns) => Boolean) - async updateFeed( - @MyUserId() userId: number, - feed: typeof FeedUnion, - ): Promise { - await this.feedService.updateFeed(userId, { - id: 1, - userPreview: { - id: 12345, - login: 'test', - imgUrl: 'testimg', - }, - at: new Date(), - type: FeedType.FOLLOW, - followed: { - id: 54321, - login: 'test2', - imgUrl: 'testimg2', - }, - }); - - return true; - } } diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 67db4759..94b33c88 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -1,27 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { FollowCacheService } from 'src/follow/follow.cache.service'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { CursorExtractor, PaginationCursorService, } from 'src/pagination/cursor/pagination.cursor.service'; -import { FeedType } from './dto/feed.dto'; -import { - BlackholedAtFeed, - EventFeed, - FeedPaginated, - FeedUnion, - FollowFeed, - LocationFeed, - NewMemberFeed, - StatusMessageFeed, - TeamStatusFinishedFeed, -} from './model/feed.model'; +import { FeedPaginated, FollowFeed, feedUnion } from './model/feed.model'; @Injectable() export class FeedService { constructor( - private readonly followCacheService: FollowCacheService, private readonly paginationCursorService: PaginationCursorService, ) {} @@ -35,28 +22,12 @@ export class FeedService { //pagination을 위해 함수 분리 //id만 가진 db를 만들어 매번 로컬피드캐시에서 join하기 <- 캐시작업때 고려 const followFeeds = await this.getFollowFeeds(userId); - const locationFeeds = await this.getLocationFeeds(userId); - const statusMessageFeeds = await this.getStatusMessageFeeds(userId); - const teamStatusFinishedFeeds = await this.getTeamStatusFinishedFeeds( - userId, - ); - const eventFeeds = await this.getEventFeeds(userId); - const newMemberFeeds = await this.getNewMemberFeeds(userId); - const blackholedAtFeeds = await this.getBlackholedAtFeeds(userId); - const feeds: (typeof FeedUnion)[] = [ - ...followFeeds, - ...locationFeeds, - ...statusMessageFeeds, - ...teamStatusFinishedFeeds, - ...eventFeeds, - ...newMemberFeeds, - ...blackholedAtFeeds, - ]; + const feeds: (typeof feedUnion)[] = [...followFeeds]; - if (!feeds.length) { - return this.generateEmptyFeed(); - } + //if (!feeds.length) { + // return this.generateEmptyFeed(); + //} //sort로 정렬 (최신순 고정) feeds.sort((a, b) => b.at.getTime() - a.at.getTime()); @@ -79,173 +50,55 @@ export class FeedService { //userId의 피드에 뜰 정보 async getFollowFeeds(userId: number): Promise { - //userId가 팔로우 한 사람들 - const followingList = await this.followCacheService.get( - userId, - 'following', - ); - - const followFeeds: FollowFeed[] = []; - - //followingList가 팔로우한 사람들 - followingList.map(async (follow) => { - const followersFollowing = await this.followCacheService.filterByDate( - follow.userPreview.id, - 'following', - follow.followAt, - ); - - followFeeds.push( - ...followersFollowing.map((follower) => { - return { - id: 1, - at: follow.followAt, - userPreview: follow.userPreview, - type: FeedType.FOLLOW, - followed: follower.userPreview, - }; - }), - ); - }); - - return followFeeds; - } - - async getLocationFeeds(userId: number): Promise { - const locationFeeds: LocationFeed[] = []; - - locationFeeds.push({ - id: 2, - at: new Date(), - userPreview: { - id: 12345, - login: 'nickname123', - imgUrl: 'profileImg123', - }, - type: FeedType.LOCATION, - location: 'c1r1s1', - }); - - return locationFeeds; - } - - async getStatusMessageFeeds(userId: number): Promise { - const statusMessageFeeds: StatusMessageFeed[] = []; - - statusMessageFeeds.push({ - id: 3, - at: new Date(), - userPreview: { - id: 23456, - login: 'nickname234', - imgUrl: 'profileImg234', - }, - type: FeedType.STATUS_MESSAGE, - message: 'status message', - }); - - return statusMessageFeeds; - } - - async getTeamStatusFinishedFeeds( - userId: number, - ): Promise { - const teamStatusFinishedFeeds: TeamStatusFinishedFeed[] = []; - - teamStatusFinishedFeeds.push({ - id: 4, - at: new Date(), - userPreview: { - id: 34567, - login: 'nickname345', - imgUrl: 'profileImg345', - }, - type: FeedType.TEAM_STATUS_FINISHED, - teamInfo: 'team status finished', - }); - - return teamStatusFinishedFeeds; - } - - async getEventFeeds(userId: number): Promise { - const eventFeeds: EventFeed[] = []; - - eventFeeds.push({ - id: 5, - at: new Date(), - userPreview: { - id: 45678, - login: 'nickname456', - imgUrl: 'profileImg456', - }, - type: FeedType.EVENT, - event: 'event', - }); - - return eventFeeds; + ////followingList가 팔로우한 사람들 + //followingList.map(async (follow) => { + // const followersFollowing = await this.followCacheService.filterByDate( + // follow.userPreview.id, + // 'following', + // follow.followAt, + // ); + + // followFeeds.push( + // ...followersFollowing.map((follower) => { + // return { + // id: 1, + // at: follow.followAt, + // userPreview: follow.userPreview, + // type: FeedType.FOLLOW, + // followed: follower.userPreview, + // }; + // }), + // ); + //}); + + return []; } - async getNewMemberFeeds(userId: number): Promise { - const newMemberFeeds: NewMemberFeed[] = []; - - newMemberFeeds.push({ - id: 6, - at: new Date(), - userPreview: { - id: 56789, - login: 'nickname567', - imgUrl: 'profileImg567', - }, - type: FeedType.NEW_MEMBER, - memberAt: new Date(), - }); - - return newMemberFeeds; - } - - async getBlackholedAtFeeds(userId: number): Promise { - const blackholedAtFeeds: BlackholedAtFeed[] = []; - - blackholedAtFeeds.push({ - id: 7, - at: new Date(), - userPreview: { - id: 67890, - login: 'nickname678', - imgUrl: 'profileImg678', - }, - type: FeedType.BLACKHOLED_AT, - blackholedAt: new Date(), - }); - - return blackholedAtFeeds; - } - - //fanout-on-write 방식 - async updateFeed(userId: number, feed: typeof FeedUnion): Promise { - //나의 팔로워 리스트를 가져옴 - const cachedFollowerList = await this.followCacheService.get( - userId, - 'follower', - ); - - //팔로워들의 feed에 새로 작성한 feed를 추가 - for (const follower of cachedFollowerList) { - console.log('write feed: ', follower.userPreview.id, feed); - //await this.feedCacheService.writeFeed(follower.userPreview.id, feed); - } - } - - private generateEmptyFeed(): FeedPaginated { - return this.paginationCursorService.toPaginated( - [], - 0, - false, - cursorExtractor, - ); - } + ////fanout-on-write 방식 + //async updateFeed(userId: number, feed: typeof FeedUnion): Promise { + // //나의 팔로워 리스트를 가져옴 + // const cachedFollowerList = await this.followCacheService.get( + // userId, + // 'follower', + // ); + + // //팔로워들의 feed에 새로 작성한 feed를 추가 + // for (const follower of cachedFollowerList) { + // console.log('write feed: ', follower.userPreview.id, feed); + // //await this.feedCacheService.writeFeed(follower.userPreview.id, feed); + // } + //} + + //private generateEmptyFeed(): FeedPaginated { + // return this.paginationCursorService.toPaginated( + // [], + // 0, + // false, + // cursorExtractor, + // ); + //} } -const cursorExtractor: CursorExtractor = (doc) => { +const cursorExtractor: CursorExtractor = (doc) => { return `${doc.id.toString()} + ${doc.at.toISOString()}`; }; diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts index b051c4d0..2062971e 100644 --- a/app/src/feed/model/feed.model.ts +++ b/app/src/feed/model/feed.model.ts @@ -1,15 +1,12 @@ import { Field, ObjectType, createUnionType } from '@nestjs/graphql'; import { UserPreview } from 'src/common/models/common.user.model'; -import { FeedType } from '../dto/feed.dto'; import { CursorPaginated } from 'src/pagination/cursor/models/pagination.cursor.model'; +import { FeedType } from '../dto/feed.dto'; @ObjectType() export class FeedBase { @Field() - id: number; - - @Field() - at: Date; + createdAt: Date; @Field((_type) => UserPreview) userPreview: UserPreview; @@ -18,8 +15,7 @@ export class FeedBase { @ObjectType() export class FollowFeed extends FeedBase { @Field((_type) => FeedType) - //todo: .FOLLOW를 적어줄 수 없음 (ㅜㅜ) - type: FeedType; + type: FeedType.FOLLOW; @Field((_type) => UserPreview) followed: UserPreview; @@ -79,7 +75,7 @@ export class BlackholedAtFeed extends FeedBase { blackholedAt: Date; } -export const FeedUnion = createUnionType({ +export const feedUnion = createUnionType({ name: 'FeedUnion', types: () => [ FollowFeed, @@ -116,4 +112,4 @@ export const FeedUnion = createUnionType({ }) as any; //todo: union을 pagination 하는 방법 찾기 @ObjectType() -export class FeedPaginated extends CursorPaginated(FeedUnion) {} +export class FeedPaginated extends CursorPaginated(feedUnion) {} diff --git a/app/src/follow/follow.module.ts b/app/src/follow/follow.module.ts index cd067fe0..c46ed4de 100644 --- a/app/src/follow/follow.module.ts +++ b/app/src/follow/follow.module.ts @@ -2,20 +2,34 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { CursusUserModule } from 'src/api/cursusUser/cursusUser.module'; import { CacheUtilModule } from 'src/cache/cache.util.module'; +import { FeedSchema, feed } from 'src/feed/db/feed.database.schema'; +import { FeedModule } from 'src/feed/feed.module'; +import { FeedService } from 'src/feed/feed.service'; import { PaginationIndexModule } from 'src/pagination/index/pagination.index.module'; import { FollowSchema, follow } from './db/follow.database.schema'; import { FollowCacheService } from './follow.cache.service'; import { FollowResolver } from './follow.resolver'; import { FollowService } from './follow.service'; +import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; @Module({ imports: [ - MongooseModule.forFeature([{ name: follow.name, schema: FollowSchema }]), + MongooseModule.forFeature([ + { name: follow.name, schema: FollowSchema }, + { name: feed.name, schema: FeedSchema }, + ]), + FeedModule, CursusUserModule, PaginationIndexModule, CacheUtilModule, ], - providers: [FollowResolver, FollowService, FollowCacheService], + providers: [ + FollowResolver, + FollowService, + FollowCacheService, + PaginationCursorService, + FeedService, + ], exports: [FollowService, FollowCacheService], }) // eslint-disable-next-line diff --git a/app/src/follow/follow.service.ts b/app/src/follow/follow.service.ts index b97f6147..f18ec924 100644 --- a/app/src/follow/follow.service.ts +++ b/app/src/follow/follow.service.ts @@ -8,6 +8,8 @@ import { findAllAndLean, findOneAndLean, } from 'src/database/mongoose/database.mongoose.query'; +import { feed } from 'src/feed/db/feed.database.schema'; +import { FeedType } from 'src/feed/dto/feed.dto'; import { PaginationIndexService } from 'src/pagination/index/pagination.index.service'; import { follow } from './db/follow.database.schema'; import { FollowSortOrder, type FollowPaginatedArgs } from './dto/follow.dto'; @@ -24,6 +26,8 @@ export class FollowService { constructor( @InjectModel(follow.name) private readonly followModel: Model, + @InjectModel(feed.name) + private readonly feedModel: Model, private readonly cursusUserCacheService: CursusUserCacheService, private readonly paginationIndexService: PaginationIndexService, private readonly followCacheService: FollowCacheService, @@ -86,6 +90,14 @@ export class FollowService { cachedfollowerList.push({ userPreview: user, followAt }); + //todo: feed 실패와 follow 실패 구분 안되는 중 + await this.feedModel.create({ + createdAt: followAt, + userPreview: user, + type: FeedType.FOLLOW, + followed: target, + }); + return { userId, followId: targetId, diff --git a/app/src/schema.gql b/app/src/schema.gql index aca46b00..401913f3 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -59,24 +59,6 @@ type UserRankingIndexPaginated { pageNumber: Int! } -type MyFollow { - isFollowing: Boolean! - userPreview: UserPreview! - followAt: DateTime! -} - -type MyFollowPaginated { - nodes: [MyFollow!]! - totalCount: Int! - pageSize: Int! - pageNumber: Int! -} - -type FollowSuccess { - userId: Int! - followId: Int! -} - type CursorPageInfo { totalCount: Int! hasNextPage: Boolean! @@ -91,8 +73,7 @@ type undefinedEdge { union FeedUnion = FollowFeed | LocationFeed | StatusMessageFeed | TeamStatusFinishedFeed | EventFeed | NewMemberFeed | BlackholedAtFeed type FollowFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! followed: UserPreview! @@ -109,48 +90,42 @@ enum FeedType { } type LocationFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! location: String! } type StatusMessageFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! message: String! } type TeamStatusFinishedFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! teamInfo: String! } type EventFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! event: String! } type NewMemberFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! memberAt: DateTime! } type BlackholedAtFeed { - id: Int! - at: DateTime! + createdAt: DateTime! userPreview: UserPreview! type: FeedType! blackholedAt: DateTime! @@ -161,6 +136,24 @@ type FeedPaginated { pageInfo: CursorPageInfo! } +type MyFollow { + isFollowing: Boolean! + userPreview: UserPreview! + followAt: DateTime! +} + +type MyFollowPaginated { + nodes: [MyFollow!]! + totalCount: Int! + pageSize: Int! + pageNumber: Int! +} + +type FollowSuccess { + userId: Int! + followId: Int! +} + type LinkableAccount { platform: String! id: String! @@ -743,7 +736,6 @@ type Mutation { deleteAccount: Int! followUser(targetId: Int!): FollowSuccess! unfollowUser(targetId: Int!): FollowSuccess! - updateFeed: Boolean! } union LoginResult = LoginSuccess | LoginNotLinked From 4465769f605082a9f27d3076a68893d2ae0933e4 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Thu, 4 Apr 2024 19:09:20 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20:recycle:=20discriminatorKey?= =?UTF-8?q?=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/db/feed.database.schema.ts | 43 +++++++++++++++++++------ app/src/feed/feed.module.ts | 19 +++++++++-- app/src/follow/follow.module.ts | 8 ++--- app/src/follow/follow.service.ts | 15 +++++---- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/app/src/feed/db/feed.database.schema.ts b/app/src/feed/db/feed.database.schema.ts index a4620637..7f0e7f18 100644 --- a/app/src/feed/db/feed.database.schema.ts +++ b/app/src/feed/db/feed.database.schema.ts @@ -3,24 +3,49 @@ import { HydratedDocument } from 'mongoose'; import { UserPreview } from 'src/common/models/common.user.model'; import { FeedType } from '../dto/feed.dto'; -export type FeedDocument = HydratedDocument; +/** feed */ -@Schema({ collection: 'feeds', discriminatorKey: 'type' }) +export type FeedDocument = HydratedDocument; +@Schema({ collection: 'feeds', discriminatorKey: 'kind' }) export class feed { + @Prop({ type: String, required: true, enum: Object.values(FeedType) }) + type: FeedType; +} + +export const FeedSchema = SchemaFactory.createForClass(feed); + +/** follow feed */ + +export type FollowFeedDocument = HydratedDocument; + +@Schema({ collection: 'feeds' }) +export class followFeed extends feed { @Prop({ required: true }) createdAt: Date; @Prop({ required: true, type: UserPreview }) userPreview: UserPreview; - @Prop({ required: true, type: String, enum: Object.values(FeedType) }) - type: FeedType; + @Prop({ required: true, type: UserPreview }) + followed: UserPreview; +} + +export const FollowFeedSchema = SchemaFactory.createForClass(followFeed); + +/** location feed */ - @Prop({ type: UserPreview }) - followed?: UserPreview; +export type LocationFeedDocument = HydratedDocument; + +@Schema({ collection: 'feeds' }) +export class locationFeed extends feed { + @Prop({ required: true }) + createdAt: Date; - @Prop({ type: String }) - location?: string; + @Prop({ required: true, type: UserPreview }) + userPreview: UserPreview; + + @Prop({ required: true }) + location: string; } -export const FeedSchema = SchemaFactory.createForClass(feed); +export const LocationFeedSchema = SchemaFactory.createForClass(locationFeed); diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts index 46f1fa82..b37c6a2b 100644 --- a/app/src/feed/feed.module.ts +++ b/app/src/feed/feed.module.ts @@ -1,13 +1,28 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; -import { FeedSchema, feed } from './db/feed.database.schema'; +import { + FeedSchema, + FollowFeedSchema, + LocationFeedSchema, + feed, +} from './db/feed.database.schema'; +import { FeedType } from './dto/feed.dto'; import { FeedResolver } from './feed.resolver'; import { FeedService } from './feed.service'; @Module({ imports: [ - MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), + MongooseModule.forFeature([ + { + name: feed.name, + schema: FeedSchema, + discriminators: [ + { name: FeedType.FOLLOW, schema: FollowFeedSchema }, + { name: FeedType.LOCATION, schema: LocationFeedSchema }, + ], + }, + ]), ], providers: [FeedResolver, FeedService, PaginationCursorService], //, FeedCacheService], exports: [FeedService], diff --git a/app/src/follow/follow.module.ts b/app/src/follow/follow.module.ts index c46ed4de..42129e83 100644 --- a/app/src/follow/follow.module.ts +++ b/app/src/follow/follow.module.ts @@ -2,21 +2,21 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { CursusUserModule } from 'src/api/cursusUser/cursusUser.module'; import { CacheUtilModule } from 'src/cache/cache.util.module'; -import { FeedSchema, feed } from 'src/feed/db/feed.database.schema'; +import { followFeed, FollowFeedSchema } from 'src/feed/db/feed.database.schema'; import { FeedModule } from 'src/feed/feed.module'; import { FeedService } from 'src/feed/feed.service'; +import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; import { PaginationIndexModule } from 'src/pagination/index/pagination.index.module'; -import { FollowSchema, follow } from './db/follow.database.schema'; +import { follow, FollowSchema } from './db/follow.database.schema'; import { FollowCacheService } from './follow.cache.service'; import { FollowResolver } from './follow.resolver'; import { FollowService } from './follow.service'; -import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; @Module({ imports: [ MongooseModule.forFeature([ { name: follow.name, schema: FollowSchema }, - { name: feed.name, schema: FeedSchema }, + { name: followFeed.name, schema: FollowFeedSchema }, ]), FeedModule, CursusUserModule, diff --git a/app/src/follow/follow.service.ts b/app/src/follow/follow.service.ts index f18ec924..32c363dc 100644 --- a/app/src/follow/follow.service.ts +++ b/app/src/follow/follow.service.ts @@ -3,12 +3,15 @@ import { InjectModel } from '@nestjs/mongoose'; import { FilterQuery, Model, SortOrder } from 'mongoose'; import { CursusUserCacheService } from 'src/api/cursusUser/cursusUser.cache.service'; import { - QueryArgs, - QueryOneArgs, findAllAndLean, findOneAndLean, + QueryArgs, + QueryOneArgs, } from 'src/database/mongoose/database.mongoose.query'; -import { feed } from 'src/feed/db/feed.database.schema'; +import { + followFeed, + FollowFeedDocument, +} from 'src/feed/db/feed.database.schema'; import { FeedType } from 'src/feed/dto/feed.dto'; import { PaginationIndexService } from 'src/pagination/index/pagination.index.service'; import { follow } from './db/follow.database.schema'; @@ -26,8 +29,8 @@ export class FollowService { constructor( @InjectModel(follow.name) private readonly followModel: Model, - @InjectModel(feed.name) - private readonly feedModel: Model, + @InjectModel(followFeed.name) + private readonly followFeedModel: Model, private readonly cursusUserCacheService: CursusUserCacheService, private readonly paginationIndexService: PaginationIndexService, private readonly followCacheService: FollowCacheService, @@ -91,7 +94,7 @@ export class FollowService { cachedfollowerList.push({ userPreview: user, followAt }); //todo: feed 실패와 follow 실패 구분 안되는 중 - await this.feedModel.create({ + await this.followFeedModel.create({ createdAt: followAt, userPreview: user, type: FeedType.FOLLOW, From 8f4f3d18caf792b597b84c348c9d43c2674926cc Mon Sep 17 00:00:00 2001 From: niamu01 Date: Fri, 5 Apr 2024 16:46:09 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20:sparkles:=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=EC=8B=9C=20feed=20db=20=EC=97=90=EC=84=9C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.module.ts | 44 +++++++++------- app/src/feed/feed.service.ts | 71 ++++++++++++-------------- app/src/follow/follow.cache.service.ts | 3 +- app/src/follow/follow.service.ts | 7 +-- 4 files changed, 61 insertions(+), 64 deletions(-) diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts index b37c6a2b..d6e74cc0 100644 --- a/app/src/feed/feed.module.ts +++ b/app/src/feed/feed.module.ts @@ -1,31 +1,37 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { FollowCacheService } from 'src/follow/follow.cache.service'; import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; -import { - FeedSchema, - FollowFeedSchema, - LocationFeedSchema, - feed, -} from './db/feed.database.schema'; -import { FeedType } from './dto/feed.dto'; +import { FeedSchema, feed } from './db/feed.database.schema'; import { FeedResolver } from './feed.resolver'; import { FeedService } from './feed.service'; @Module({ + //imports: [ + // MongooseModule.forFeature([ + // { + // name: feed.name, + // schema: FeedSchema, + // discriminators: [ + // { name: followFeed.name, schema: FollowFeedSchema }, + // { name: locationFeed.name, schema: LocationFeedSchema }, + // ], + // }, + // ]), + //], imports: [ - MongooseModule.forFeature([ - { - name: feed.name, - schema: FeedSchema, - discriminators: [ - { name: FeedType.FOLLOW, schema: FollowFeedSchema }, - { name: FeedType.LOCATION, schema: LocationFeedSchema }, - ], - }, - ]), + MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), + ], + providers: [ + FeedResolver, + FeedService, + PaginationCursorService, + FollowCacheService, + ], + exports: [ + FeedService, + MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), ], - providers: [FeedResolver, FeedService, PaginationCursorService], //, FeedCacheService], - exports: [FeedService], }) // eslint-disable-next-line diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 94b33c88..3abacc28 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -1,15 +1,22 @@ import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { FollowCacheService } from 'src/follow/follow.cache.service'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { CursorExtractor, PaginationCursorService, } from 'src/pagination/cursor/pagination.cursor.service'; -import { FeedPaginated, FollowFeed, feedUnion } from './model/feed.model'; +import { feed } from './db/feed.database.schema'; +import { FeedPaginated, feedUnion } from './model/feed.model'; @Injectable() export class FeedService { constructor( + @InjectModel(feed.name) + private readonly feedModel: Model, private readonly paginationCursorService: PaginationCursorService, + private readonly followCacheService: FollowCacheService, ) {} async getFeedPaginated({ @@ -19,20 +26,8 @@ export class FeedService { userId: number; args: PaginationCursorArgs; }): Promise { - //pagination을 위해 함수 분리 - //id만 가진 db를 만들어 매번 로컬피드캐시에서 join하기 <- 캐시작업때 고려 - const followFeeds = await this.getFollowFeeds(userId); + const feeds = await this.getFeeds(userId); - const feeds: (typeof feedUnion)[] = [...followFeeds]; - - //if (!feeds.length) { - // return this.generateEmptyFeed(); - //} - - //sort로 정렬 (최신순 고정) - feeds.sort((a, b) => b.at.getTime() - a.at.getTime()); - - //pagination if (args.after) { const afterIndex = feeds.findIndex( (feed) => cursorExtractor(feed) === args.after, @@ -48,30 +43,31 @@ export class FeedService { ); } - //userId의 피드에 뜰 정보 - async getFollowFeeds(userId: number): Promise { - ////followingList가 팔로우한 사람들 - //followingList.map(async (follow) => { - // const followersFollowing = await this.followCacheService.filterByDate( - // follow.userPreview.id, - // 'following', - // follow.followAt, - // ); + async getFeeds(userId: number): Promise<(typeof feedUnion)[]> { + const followList = await this.followCacheService.get(userId, 'following'); + + const conditions = followList.map((follow) => ({ + 'userPreview.id': follow.userPreview.id, + createdAt: { $gt: follow.followAt }, + })); - // followFeeds.push( - // ...followersFollowing.map((follower) => { - // return { - // id: 1, - // at: follow.followAt, - // userPreview: follow.userPreview, - // type: FeedType.FOLLOW, - // followed: follower.userPreview, - // }; - // }), - // ); - //}); + const feeds = await Promise.all( + conditions.map(async (condition) => { + const aggregate = this.feedModel.aggregate(); - return []; + const feed = await aggregate + .match(condition) + .sort({ createdAt: -1 }) + .project({ + _id: 0, + __v: 0, + }); + + return feed; + }), + ); + + return feeds.reduce((acc, curr) => acc.concat(curr), []); } ////fanout-on-write 방식 @@ -96,9 +92,8 @@ export class FeedService { // false, // cursorExtractor, // ); - //} } const cursorExtractor: CursorExtractor = (doc) => { - return `${doc.id.toString()} + ${doc.at.toISOString()}`; + return `${doc.userPreview.id.toString()} + ${doc.createdAt.toISOString()}`; }; diff --git a/app/src/follow/follow.cache.service.ts b/app/src/follow/follow.cache.service.ts index fbc6180f..ccec3a64 100644 --- a/app/src/follow/follow.cache.service.ts +++ b/app/src/follow/follow.cache.service.ts @@ -38,8 +38,7 @@ export class FollowCacheService { return cachedData; } - //todo: cacheUtilService에 추가? - async filterByDate( + async getByDate( userId: number, type: 'follower' | 'following', time: Date, diff --git a/app/src/follow/follow.service.ts b/app/src/follow/follow.service.ts index 32c363dc..2c7fa720 100644 --- a/app/src/follow/follow.service.ts +++ b/app/src/follow/follow.service.ts @@ -8,10 +8,7 @@ import { QueryArgs, QueryOneArgs, } from 'src/database/mongoose/database.mongoose.query'; -import { - followFeed, - FollowFeedDocument, -} from 'src/feed/db/feed.database.schema'; +import { followFeed } from 'src/feed/db/feed.database.schema'; import { FeedType } from 'src/feed/dto/feed.dto'; import { PaginationIndexService } from 'src/pagination/index/pagination.index.service'; import { follow } from './db/follow.database.schema'; @@ -30,7 +27,7 @@ export class FollowService { @InjectModel(follow.name) private readonly followModel: Model, @InjectModel(followFeed.name) - private readonly followFeedModel: Model, + private readonly followFeedModel: Model, private readonly cursusUserCacheService: CursusUserCacheService, private readonly paginationIndexService: PaginationIndexService, private readonly followCacheService: FollowCacheService, From af1e3fc9c5203d1741306ed50589c4b866c351b1 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Sun, 7 Apr 2024 16:23:32 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20:recycle:=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=20=ED=95=A9=EC=B9=98=EB=8A=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?flat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flatMap은 빈 배열을 합치지 못하더라구용 - #416 --- app/src/feed/feed.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index 3abacc28..cda5253c 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -67,7 +67,7 @@ export class FeedService { }), ); - return feeds.reduce((acc, curr) => acc.concat(curr), []); + return feeds.flat(); } ////fanout-on-write 방식 From e4d1045a7215454208c43053a299795cad3d5497 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Fri, 12 Apr 2024 20:17:39 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20:sparkles:=20=EC=B5=9C=EA=B7=BC?= =?UTF-8?q?=201=EB=8B=AC=EA=B0=84=EC=9D=98=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #416 --- app/src/feed/feed.cache.service.ts | 48 +++++++++++++++++++----------- app/src/feed/feed.module.ts | 4 +++ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts index 5014c933..3440fa3c 100644 --- a/app/src/feed/feed.cache.service.ts +++ b/app/src/feed/feed.cache.service.ts @@ -1,22 +1,36 @@ -// import { Injectable } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Cron } from '@nestjs/schedule'; +import { Model } from 'mongoose'; +import { CacheUtilService } from 'src/cache/cache.util.service'; +import { DateWrapper } from 'src/dateWrapper/dateWrapper'; +import { feed } from './db/feed.database.schema'; -// @Injectable() -// export class FeedCacheService { -// constructor() {} +@Injectable() +export class FeedCacheService { + constructor( + @InjectModel(feed.name) + private readonly feedModel: Model, + @Inject(CACHE_MANAGER) + private readonly cacheUtilService: CacheUtilService, + ) {} -// private feedCache = new Map(); + /** + * @description 매일 05:00에 최신 한달간의 피드를 캐싱한다. (ttl: 1일) + * todo test: 1분 간격으로 cron 실행 + */ + @Cron('*/1 * * * *') + async monthlyFeedCache() { + const lastMonth = new DateWrapper().moveMonth(-1).toDate(); -// async get(userId: number): Promise<(typeof FeedUnion)[]> { -// return this.feedCache.get(userId) ?? []; -// } + const lastMonthFeeds = await this.feedModel.aggregate([ + { $match: { createdAt: { $gte: lastMonth } } }, + { $sort: { createdAt: -1 } }, + ]); -// async set(userId: number, feed: (typeof FeedUnion)[]): Promise { -// this.feedCache.set(userId, feed); -// } + const key = `lastMonthFeeds:${lastMonth}`; -// async writeFeed(userId: number, feed: typeof FeedUnion): Promise { -// const prev = await this.get(userId); - -// await this.set(userId, [...prev, feed]); -// } -// } + await this.cacheUtilService.set(key, lastMonthFeeds, DateWrapper.DAY); + } +} diff --git a/app/src/feed/feed.module.ts b/app/src/feed/feed.module.ts index d6e74cc0..00504004 100644 --- a/app/src/feed/feed.module.ts +++ b/app/src/feed/feed.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { CacheUtilModule } from 'src/cache/cache.util.module'; import { FollowCacheService } from 'src/follow/follow.cache.service'; import { PaginationCursorService } from 'src/pagination/cursor/pagination.cursor.service'; import { FeedSchema, feed } from './db/feed.database.schema'; +import { FeedCacheService } from './feed.cache.service'; import { FeedResolver } from './feed.resolver'; import { FeedService } from './feed.service'; @@ -21,11 +23,13 @@ import { FeedService } from './feed.service'; //], imports: [ MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]), + CacheUtilModule, ], providers: [ FeedResolver, FeedService, PaginationCursorService, + FeedCacheService, FollowCacheService, ], exports: [ From 1eee93f49515428819b70a941222ee2e19988d94 Mon Sep 17 00:00:00 2001 From: niamu01 Date: Fri, 12 Apr 2024 22:02:51 +0900 Subject: [PATCH 13/13] =?UTF-8?q?feat:=20:sparkles:=20totalCount=EA=B0=80?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20pagination=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 틀만 갖추도록 만들어 두고 endCursor도 없어질 예정 - #416 --- app/src/feed/feed.cache.service.ts | 3 +- app/src/feed/feed.resolver.ts | 6 +- app/src/feed/feed.service.ts | 89 +++++++++++++----------------- app/src/feed/model/feed.model.ts | 29 +++++++++- app/src/schema.gql | 19 ++++--- 5 files changed, 82 insertions(+), 64 deletions(-) diff --git a/app/src/feed/feed.cache.service.ts b/app/src/feed/feed.cache.service.ts index 3440fa3c..f9ddca80 100644 --- a/app/src/feed/feed.cache.service.ts +++ b/app/src/feed/feed.cache.service.ts @@ -27,9 +27,10 @@ export class FeedCacheService { const lastMonthFeeds = await this.feedModel.aggregate([ { $match: { createdAt: { $gte: lastMonth } } }, { $sort: { createdAt: -1 } }, + { $project: { _id: 0, __v: 0 } }, ]); - const key = `lastMonthFeeds:${lastMonth}`; + const key = `lastMonthFeeds`; await this.cacheUtilService.set(key, lastMonthFeeds, DateWrapper.DAY); } diff --git a/app/src/feed/feed.resolver.ts b/app/src/feed/feed.resolver.ts index 7f84cc4b..77e8d0c2 100644 --- a/app/src/feed/feed.resolver.ts +++ b/app/src/feed/feed.resolver.ts @@ -4,17 +4,17 @@ import { MyUserId } from 'src/auth/myContext'; import { StatAuthGuard } from 'src/auth/statAuthGuard'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { FeedService } from './feed.service'; -import { FeedPaginated } from './model/feed.model'; +import { FeedPage } from './model/feed.model'; @UseGuards(StatAuthGuard) @Resolver() export class FeedResolver { constructor(private readonly feedService: FeedService) {} - @Query((_returns) => FeedPaginated) + @Query((_returns) => FeedPage) async getFeed( @MyUserId() userId: number, @Args() args: PaginationCursorArgs, - ): Promise { + ): Promise { return await this.feedService.getFeedPaginated({ userId, args }); } } diff --git a/app/src/feed/feed.service.ts b/app/src/feed/feed.service.ts index cda5253c..56c610ef 100644 --- a/app/src/feed/feed.service.ts +++ b/app/src/feed/feed.service.ts @@ -1,6 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; +import { CacheUtilService } from 'src/cache/cache.util.service'; import { FollowCacheService } from 'src/follow/follow.cache.service'; import { PaginationCursorArgs } from 'src/pagination/cursor/dtos/pagination.cursor.dto'; import { @@ -8,7 +10,7 @@ import { PaginationCursorService, } from 'src/pagination/cursor/pagination.cursor.service'; import { feed } from './db/feed.database.schema'; -import { FeedPaginated, feedUnion } from './model/feed.model'; +import { FeedEdge, FeedPage, feedUnion, PageInfo } from './model/feed.model'; @Injectable() export class FeedService { @@ -17,6 +19,8 @@ export class FeedService { private readonly feedModel: Model, private readonly paginationCursorService: PaginationCursorService, private readonly followCacheService: FollowCacheService, + @Inject(CACHE_MANAGER) + private readonly cacheUtilService: CacheUtilService, ) {} async getFeedPaginated({ @@ -25,9 +29,11 @@ export class FeedService { }: { userId: number; args: PaginationCursorArgs; - }): Promise { + }): Promise { const feeds = await this.getFeeds(userId); + console.log(feeds); + if (args.after) { const afterIndex = feeds.findIndex( (feed) => cursorExtractor(feed) === args.after, @@ -35,63 +41,46 @@ export class FeedService { feeds.splice(0, afterIndex + 1); } - return this.paginationCursorService.toPaginated( - feeds.slice(0, args.first), - feeds.length, - feeds.length > args.first, - cursorExtractor, - ); + const edges: FeedEdge[] = feeds.map((feed) => ({ + node: feed, + cursor: cursorExtractor(feed), + })); + + const pageInfo: PageInfo = { + hasNextPage: feeds.length > args.first, + endCursor: edges.at(-1)?.cursor, + }; + + return { + edges, + pageInfo, + }; } async getFeeds(userId: number): Promise<(typeof feedUnion)[]> { const followList = await this.followCacheService.get(userId, 'following'); - const conditions = followList.map((follow) => ({ - 'userPreview.id': follow.userPreview.id, - createdAt: { $gt: follow.followAt }, - })); - - const feeds = await Promise.all( - conditions.map(async (condition) => { - const aggregate = this.feedModel.aggregate(); + const key = `lastMonthFeeds`; + const feeds = await this.cacheUtilService.get<(typeof feedUnion)[]>(key); - const feed = await aggregate - .match(condition) - .sort({ createdAt: -1 }) - .project({ - _id: 0, - __v: 0, - }); + //cache가 없는 경우 고려 + if (!feeds) { + console.log('cache miss'); + return []; + } - return feed; - }), - ); + feeds.filter((feed) => { + const isFollowedUser = followList.some( + (follow) => follow.userPreview.id === feed.userPreview.id, + ); + const isAfterFollow = followList.some( + (follow) => follow.followAt < feed.createdAt, + ); + return isFollowedUser && isAfterFollow; + }); return feeds.flat(); } - - ////fanout-on-write 방식 - //async updateFeed(userId: number, feed: typeof FeedUnion): Promise { - // //나의 팔로워 리스트를 가져옴 - // const cachedFollowerList = await this.followCacheService.get( - // userId, - // 'follower', - // ); - - // //팔로워들의 feed에 새로 작성한 feed를 추가 - // for (const follower of cachedFollowerList) { - // console.log('write feed: ', follower.userPreview.id, feed); - // //await this.feedCacheService.writeFeed(follower.userPreview.id, feed); - // } - //} - - //private generateEmptyFeed(): FeedPaginated { - // return this.paginationCursorService.toPaginated( - // [], - // 0, - // false, - // cursorExtractor, - // ); } const cursorExtractor: CursorExtractor = (doc) => { diff --git a/app/src/feed/model/feed.model.ts b/app/src/feed/model/feed.model.ts index 2062971e..f69c3a7e 100644 --- a/app/src/feed/model/feed.model.ts +++ b/app/src/feed/model/feed.model.ts @@ -1,6 +1,5 @@ import { Field, ObjectType, createUnionType } from '@nestjs/graphql'; import { UserPreview } from 'src/common/models/common.user.model'; -import { CursorPaginated } from 'src/pagination/cursor/models/pagination.cursor.model'; import { FeedType } from '../dto/feed.dto'; @ObjectType() @@ -109,7 +108,31 @@ export const feedUnion = createUnionType({ return BlackholedAtFeed; } }, -}) as any; //todo: union을 pagination 하는 방법 찾기 +}); @ObjectType() -export class FeedPaginated extends CursorPaginated(feedUnion) {} +export class PageInfo { + @Field() + hasNextPage: boolean; + + @Field({ nullable: true }) + endCursor?: String; +} + +@ObjectType() +export class FeedEdge { + @Field() + cursor: string; + + @Field((_type) => feedUnion) + node: typeof feedUnion; +} + +@ObjectType() +export class FeedPage { + @Field((_type) => [FeedEdge]) + edges: FeedEdge[]; + + @Field() + pageInfo: PageInfo; +} diff --git a/app/src/schema.gql b/app/src/schema.gql index 401913f3..c4f539cc 100644 --- a/app/src/schema.gql +++ b/app/src/schema.gql @@ -59,13 +59,12 @@ type UserRankingIndexPaginated { pageNumber: Int! } -type CursorPageInfo { - totalCount: Int! +type PageInfo { hasNextPage: Boolean! endCursor: String } -type undefinedEdge { +type FeedEdge { cursor: String! node: FeedUnion! } @@ -131,9 +130,9 @@ type BlackholedAtFeed { blackholedAt: DateTime! } -type FeedPaginated { - edges: [undefinedEdge!]! - pageInfo: CursorPageInfo! +type FeedPage { + edges: [FeedEdge!]! + pageInfo: PageInfo! } type MyFollow { @@ -187,6 +186,12 @@ type ProjectPreview { difficulty: Int } +type CursorPageInfo { + totalCount: Int! + hasNextPage: Boolean! + endCursor: String +} + type TeamPreview { id: Int! name: String! @@ -713,7 +718,7 @@ type Query { getIsFollowing(targetId: Int!): Boolean! getFollowerPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! getFollowingPaginated(pageSize: Int! = 10, pageNumber: Int! = 1, targetId: Int!, sortOrder: FollowSortOrder! = FOLLOW_AT_DESC): MyFollowPaginated! - getFeed(after: String, first: Int! = 20): FeedPaginated! + getFeed(after: String, first: Int! = 20): FeedPage! } enum EvalLogSortOrder {