Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -107,6 +108,7 @@ export class AppRootModule {}
SettingModule,
CalculatorModule,
FollowModule,
FeedModule,
LambdaModule,
HealthCheckModule,
CacheDecoratorOnReturnModule,
Expand Down
51 changes: 51 additions & 0 deletions app/src/feed/db/feed.database.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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';

/** feed */

export type FeedDocument = HydratedDocument<feed>;
@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<followFeed>;

@Schema({ collection: 'feeds' })
export class followFeed extends feed {
@Prop({ required: true })
createdAt: Date;

@Prop({ required: true, type: UserPreview })
userPreview: UserPreview;

@Prop({ required: true, type: UserPreview })
followed: UserPreview;
}

export const FollowFeedSchema = SchemaFactory.createForClass(followFeed);

/** location feed */

export type LocationFeedDocument = HydratedDocument<followFeed>;

@Schema({ collection: 'feeds' })
export class locationFeed extends feed {
@Prop({ required: true })
createdAt: Date;

@Prop({ required: true, type: UserPreview })
userPreview: UserPreview;

@Prop({ required: true })
location: string;
}

export const LocationFeedSchema = SchemaFactory.createForClass(locationFeed);
13 changes: 13 additions & 0 deletions app/src/feed/dto/feed.dto.ts
Original file line number Diff line number Diff line change
@@ -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' });
37 changes: 37 additions & 0 deletions app/src/feed/feed.cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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(
@InjectModel(feed.name)
private readonly feedModel: Model<feed>,
@Inject(CACHE_MANAGER)
private readonly cacheUtilService: CacheUtilService,
) {}

/**
* @description 매일 05:00에 최신 한달간의 피드를 캐싱한다. (ttl: 1일)
* todo test: 1분 간격으로 cron 실행
*/
@Cron('*/1 * * * *')
async monthlyFeedCache() {
const lastMonth = new DateWrapper().moveMonth(-1).toDate();

const lastMonthFeeds = await this.feedModel.aggregate([
{ $match: { createdAt: { $gte: lastMonth } } },
{ $sort: { createdAt: -1 } },
{ $project: { _id: 0, __v: 0 } },
]);

const key = `lastMonthFeeds`;

await this.cacheUtilService.set(key, lastMonthFeeds, DateWrapper.DAY);
}
}
42 changes: 42 additions & 0 deletions app/src/feed/feed.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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';

@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 }]),
CacheUtilModule,
],
providers: [
FeedResolver,
FeedService,
PaginationCursorService,
FeedCacheService,
FollowCacheService,
],
exports: [
FeedService,
MongooseModule.forFeature([{ name: feed.name, schema: FeedSchema }]),
],
})

// eslint-disable-next-line
export class FeedModule {}
20 changes: 20 additions & 0 deletions app/src/feed/feed.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UseGuards } from '@nestjs/common';
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 { FeedService } from './feed.service';
import { FeedPage } from './model/feed.model';

@UseGuards(StatAuthGuard)
@Resolver()
export class FeedResolver {
constructor(private readonly feedService: FeedService) {}
@Query((_returns) => FeedPage)
async getFeed(
@MyUserId() userId: number,
@Args() args: PaginationCursorArgs,
): Promise<FeedPage> {
return await this.feedService.getFeedPaginated({ userId, args });
}
}
88 changes: 88 additions & 0 deletions app/src/feed/feed.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 {
CursorExtractor,
PaginationCursorService,
} from 'src/pagination/cursor/pagination.cursor.service';
import { feed } from './db/feed.database.schema';
import { FeedEdge, FeedPage, feedUnion, PageInfo } from './model/feed.model';

@Injectable()
export class FeedService {
constructor(
@InjectModel(feed.name)
private readonly feedModel: Model<feed>,
private readonly paginationCursorService: PaginationCursorService,
private readonly followCacheService: FollowCacheService,
@Inject(CACHE_MANAGER)
private readonly cacheUtilService: CacheUtilService,
) {}

async getFeedPaginated({
userId,
args,
}: {
userId: number;
args: PaginationCursorArgs;
}): Promise<FeedPage> {
const feeds = await this.getFeeds(userId);

console.log(feeds);

if (args.after) {
const afterIndex = feeds.findIndex(
(feed) => cursorExtractor(feed) === args.after,
);
feeds.splice(0, afterIndex + 1);
}

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 key = `lastMonthFeeds`;
const feeds = await this.cacheUtilService.get<(typeof feedUnion)[]>(key);

//cache가 없는 경우 고려
if (!feeds) {
console.log('cache miss');
return [];
}

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();
}
}

const cursorExtractor: CursorExtractor<typeof feedUnion> = (doc) => {
return `${doc.userPreview.id.toString()} + ${doc.createdAt.toISOString()}`;
};
Loading