Skip to content

Commit

Permalink
Merge branch 'development' into feat/folder-list-api
Browse files Browse the repository at this point in the history
  • Loading branch information
phil-classum committed Jul 3, 2024
2 parents 5b7174a + 7fafc20 commit 3c17870
Show file tree
Hide file tree
Showing 31 changed files with 1,541 additions and 897 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# compiled output
/dist
/worker-dist
/node_modules

# Logs
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
"license": "UNLICENSED",
"scripts": {
"prebuild": "if [ -d dist ]; then rm -r dist; fi",
"prebuild:worker": "if [ -d worker-dist ]; then rm -r worker-dist; fi",
"build": "npx webpack",
"deploy": "npm run build && sls deploy && npm run postdeploy",
"build:worker": "pnpm run prebuild:worker && npx webpack -c worker-webpack.config.ts",
"deploy": "npm run build && sls deploy function -f server && npm run postdeploy",
"deploy:worker": "npm run build:worker && sls deploy function -f aiWorker && npm run postdeploy",
"deploy:all": "npm run build && npm run build:worker && sls deploy && npm run postdeploy",
"postdeploy": "if [ -d .serverless ]; then rm -r .serverless; fi",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"db:up": "docker-compose up -d",
Expand All @@ -23,6 +27,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-lambda": "^3.606.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
Expand Down
1,981 changes: 1,109 additions & 872 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ functions:
path: '{proxy+}'
cors:
origin: '*'
aiWorker:
handler: worker-dist/index.handler

package:
exclude:
- '**/*'
include:
- 'dist/**'
- 'worker-dist/**'
23 changes: 23 additions & 0 deletions src/ai_handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Context, Handler } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { AiModule } from '@src/infrastructure/ai/ai.module';
import { AiService } from '@src/infrastructure/ai/ai.service';

export const handler: Handler = async (event: any) => {
const data = JSON.parse(event.body);
// event 데이터 잘 들어오는지 확인용
console.log(data);
const app = await NestFactory.create(AiModule);
const aiService = app.get(AiService);

// NOTE: AI 요약 요청
const summarizeUrlContent = await aiService.summarizeLinkContent(
data.postContent,
data.folderList,
);

if (summarizeUrlContent.success === true) {
return true;
// TODO create row PostAIClassification
}
};
4 changes: 4 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from '@src/infrastructure';
import { AiModule } from './infrastructure/ai/ai.module';
import { UsersModule } from './modules/users/users.module';
import { ClassificationModule } from './modules/classification/classification.module';
import { AuthModule } from './modules/auth/auth.module';
import { FoldersModule } from './modules/folders/folders.module';
import { LinksModule } from './modules/links/links.module';
import { PostsModule } from './modules/posts/posts.module';
import { AwsLambdaModule } from './infrastructure/aws-lambda/aws-lambda.module';

@Module({
imports: [
Expand All @@ -23,10 +25,12 @@ import { PostsModule } from './modules/posts/posts.module';
DatabaseModule,
AiModule,
UsersModule,
ClassificationModule,
AuthModule,
FoldersModule,
LinksModule,
PostsModule,
AwsLambdaModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
1 change: 0 additions & 1 deletion src/common/decorators/getUser.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const GetUser = createParamDecorator(
): ReqUserPayload | ReqUserPayload[keyof ReqUserPayload] => {
// Get context Request Object
const request = context.switchToHttp().getRequest<Request>();

// Expect Request, user property as type 'ReqUserPayload'(Refer to defined in common/types/type.d.ts)
const user: Express.User = request.user;
return user['id'];
Expand Down
2 changes: 2 additions & 0 deletions src/infrastructure/ai/ai.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common';
import { AiService } from './ai.service';
import { DatabaseModule } from '@src/infrastructure';

@Module({
imports: [DatabaseModule],
providers: [AiService],
exports: [AiService],
})
Expand Down
8 changes: 8 additions & 0 deletions src/infrastructure/aws-lambda/aws-lambda.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { AwsLambdaService } from './aws-lambda.service';

@Module({
providers: [AwsLambdaService],
exports: [AwsLambdaService],
})
export class AwsLambdaModule {}
24 changes: 24 additions & 0 deletions src/infrastructure/aws-lambda/aws-lambda.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AwsLambdaService {
constructor(private readonly config: ConfigService) {}
// TODO credentials 추가 필요 (acccess_key, secret_key)
readonly client = new LambdaClient({
region: 'ap-northeast-2',
credentials: {
accessKeyId: this.config.get<string>('AWS_ACCESS_KEY'),
secretAccessKey: this.config.get<string>('AWS_SECRET_KEY'),
},
});

invoke_lambda(lambdaFunctionName: string, payload: object): void {
const command = new InvokeCommand({
FunctionName: lambdaFunctionName,
InvocationType: 'Event',
Payload: JSON.stringify(payload),
});
this.client.send(command);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Schema as MongooseSchema } from 'mongoose';
import {
Keyword,
KeywordSchema,
} from '@src/infrastructure/database/schema/keyword.schema';

import { BaseDocument } from './base.schema';

@Schema({
collection: 'post_ai_classifications',
collection: 'ai_classifications',
timestamps: true,
versionKey: false,
})
export class PostAIClassification {
export class AIClassification extends BaseDocument {
@Prop({ required: true, type: MongooseSchema.Types.ObjectId, ref: 'Folder' })
suggestedFolderId: MongooseSchema.Types.ObjectId;

Expand All @@ -20,17 +18,16 @@ export class PostAIClassification {
@Prop({ required: true })
description: string;

@Prop({ required: true, type: [KeywordSchema] })
keywords: Keyword[];
@Prop({ required: true, type: [String] })
keywords: string[];

@Prop({ type: Date })
completedAt: Date;

@Prop({ type: Date })
@Prop({ default: null })
deletedAt: Date;
}

export type PostAIClassificationDocument =
HydratedDocument<PostAIClassification>;
export type PostAIClassificationDocument = HydratedDocument<AIClassification>;
export const PostAIClassificationSchema =
SchemaFactory.createForClass(PostAIClassification);
SchemaFactory.createForClass(AIClassification);
4 changes: 2 additions & 2 deletions src/infrastructure/database/schema/base.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class BaseDocument {
createdAt: string;
createdAt: Date;

updatedAt: string;
updatedAt: Date;
}
2 changes: 1 addition & 1 deletion src/infrastructure/database/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export * from './user.schema';
export * from './folder.schema';
export * from './keyword.schema';
export * from './post.schema';
export * from './postAIClassification.schema';
export * from './AIClassification.schema';
export * from './postUserRead.schema';
5 changes: 3 additions & 2 deletions src/infrastructure/database/schema/post.schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseDocument } from './base.schema';
import { AIClassification } from './AIClassification.schema';

@Schema({ collection: 'posts', timestamps: true, versionKey: false })
export class Post extends BaseDocument {
Expand All @@ -25,9 +26,9 @@ export class Post extends BaseDocument {
@Prop({
required: false,
type: MongooseSchema.Types.ObjectId,
ref: 'PostAIClassification',
ref: 'AIClassification',
})
aiClassificationId?: MongooseSchema.Types.ObjectId;
aiClassificationId?: MongooseSchema.Types.ObjectId | AIClassification;
}

export type PostDocument = HydratedDocument<Post>;
Expand Down
41 changes: 41 additions & 0 deletions src/modules/classification/classification.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Controller, Get, UseGuards, Param } from '@nestjs/common';
import { ClassificationService } from './classification.service';
import { GetUser } from '@src/common';
import {
ClassificationControllerDocs,
GetAIFolderNameListDocs,
GetAIPostListDocs,
} from './docs';
import { JwtGuard } from '../users/guards';
import { AIFolderNameListResponse } from './response/ai-folder-list.dto';
import { AIPostListResponse } from './response/ai-post-list.dto';

@Controller('classification')
@UseGuards(JwtGuard)
@ClassificationControllerDocs
export class ClassificationController {
constructor(private readonly classificationService: ClassificationService) {}

@Get('/suggestions') //TODO : 정렬
@GetAIFolderNameListDocs
async getSuggestedFolderNameList(@GetUser() userId: String) {
const folderNames =
await this.classificationService.getFolderNameList(userId);

return new AIFolderNameListResponse(folderNames);
}

@Get('/suggestions/:folderId')
@GetAIPostListDocs
async getSuggestedPostList(
@GetUser() userId: string,
@Param('folderId') folderId: string,
) {
const posts = await this.classificationService.getPostList(
userId,
folderId,
);

return new AIPostListResponse(posts);
}
}
26 changes: 26 additions & 0 deletions src/modules/classification/classification.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { ClassificationService } from './classification.service';
import { MongooseModule } from '@nestjs/mongoose';
import {
Folder,
FolderSchema,
Post,
AIClassification,
PostAIClassificationSchema,
PostSchema,
} from '@src/infrastructure/database/schema';

import { ClassificationController } from './classification.controller';

@Module({
imports: [
MongooseModule.forFeature([
{ name: Folder.name, schema: FolderSchema },
{ name: AIClassification.name, schema: PostAIClassificationSchema },
{ name: Post.name, schema: PostSchema },
]),
],
controllers: [ClassificationController],
providers: [ClassificationService],
})
export class ClassificationModule {}
18 changes: 18 additions & 0 deletions src/modules/classification/classification.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ClassificationService } from './classification.service';

describe('ClassificationService', () => {
let service: ClassificationService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ClassificationService],
}).compile();

service = module.get<ClassificationService>(ClassificationService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
58 changes: 58 additions & 0 deletions src/modules/classification/classification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import {
Folder,
Post,
AIClassification,
} from '@src/infrastructure/database/schema';

import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { AIFolderNameServiceDto } from './dto/getAIFolderNameLIst.dto';
import { AIPostServiceDto } from './dto/getAIPostList.dto';

@Injectable()
export class ClassificationService {
constructor(
@InjectModel(Folder.name) private folderModel: Model<Folder>,
@InjectModel(AIClassification.name)
private postAiClassificationModel: Model<AIClassification>,
@InjectModel(Post.name) private postModel: Model<Post>,
) {}

async getFolderNameList(userId: String): Promise<AIFolderNameServiceDto[]> {
const folders = await this.folderModel.find({ userId }).exec();
const folderIds = folders.map((folder) => folder._id);

const classificationIds = await this.postAiClassificationModel
.distinct('suggestedFolderId')
.where('suggestedFolderId')
.in(folderIds)
.exec();

const matchedFolders = await this.folderModel
.find({ _id: { $in: classificationIds } })
.exec();

return matchedFolders.map((folder) => new AIFolderNameServiceDto(folder));
}

async getPostList(
userId: string,
folderId: string,
): Promise<AIPostServiceDto[]> {
const posts = await this.postModel
.find({ userId: userId })
.populate<{
aiClassificationId: AIClassification;
}>({
path: 'aiClassificationId',
match: { deletedAt: null, suggestedFolderId: folderId },
})
.sort({ createdAt: -1 })
.exec();

return posts
.filter((post) => post.aiClassificationId)
.map((post) => new AIPostServiceDto(post, post.aiClassificationId));
}
}
6 changes: 6 additions & 0 deletions src/modules/classification/docs/controller.docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { applyDecorators } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

export const ClassificationControllerDocs = applyDecorators(
ApiTags('AI classification API'),
);
14 changes: 14 additions & 0 deletions src/modules/classification/docs/getAIFolderNameList.docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AIFolderNameListResponse } from '../response/ai-folder-list.dto';

export const GetAIFolderNameListDocs = applyDecorators(
ApiOperation({
summary: '폴더 리스트',
description: 'AI 분류 폴더 리스트.',
}),
ApiResponse({
type: AIFolderNameListResponse,
}),
ApiBearerAuth(),
);
Loading

0 comments on commit 3c17870

Please sign in to comment.