From 31582b3ddbf533dbf2a04239c0773925f8a48aec Mon Sep 17 00:00:00 2001 From: Geonoing Date: Thu, 18 Jul 2024 02:41:22 +0900 Subject: [PATCH 01/19] chore: apply auto organize --- .vscode/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0600eab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } +} From 4a8cdce844e61ec24dff3f8712fe02b042ed7a86 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Thu, 18 Jul 2024 02:42:19 +0900 Subject: [PATCH 02/19] chore: add discord alarm --- src/infrastructure/ai/ai.module.ts | 4 +-- src/infrastructure/ai/ai.service.ts | 25 ++++++++++++++++--- .../discord/discord-webhook.provider.ts | 17 +++++++++++++ src/infrastructure/discord/discord.module.ts | 8 ++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/infrastructure/discord/discord-webhook.provider.ts create mode 100644 src/infrastructure/discord/discord.module.ts diff --git a/src/infrastructure/ai/ai.module.ts b/src/infrastructure/ai/ai.module.ts index 2763da8..65f0f99 100644 --- a/src/infrastructure/ai/ai.module.ts +++ b/src/infrastructure/ai/ai.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; +import { DiscordModule } from '../discord/discord.module'; import { AiService } from './ai.service'; -import { DatabaseModule } from '@src/infrastructure'; @Module({ - imports: [DatabaseModule], + imports: [DiscordModule], providers: [AiService], exports: [AiService], }) diff --git a/src/infrastructure/ai/ai.service.ts b/src/infrastructure/ai/ai.service.ts index 43d0234..9777976 100644 --- a/src/infrastructure/ai/ai.service.ts +++ b/src/infrastructure/ai/ai.service.ts @@ -1,16 +1,20 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import OpenAI, { RateLimitError, OpenAIError } from 'openai'; -import { summarizeURLContentFunctionFactory } from './functions'; +import OpenAI, { OpenAIError, RateLimitError } from 'openai'; +import { DiscordWebhookProvider } from '../discord/discord-webhook.provider'; +import { gptVersion } from './ai.constant'; import { SummarizeURLContentDto } from './dto'; -import { gptVersion, mockFolderLists } from './ai.constant'; +import { summarizeURLContentFunctionFactory } from './functions'; import { SummarizeURLContent } from './types/types'; @Injectable() export class AiService { private openai: OpenAI; - constructor(private readonly config: ConfigService) { + constructor( + private readonly config: ConfigService, + private readonly discordProvider: DiscordWebhookProvider, + ) { this.openai = new OpenAI({ apiKey: config.get('OPENAI_API_KEY'), }); @@ -30,6 +34,7 @@ export class AiService { folderLists, temperature, ); + // Function Call 결과 const summaryResult: SummarizeURLContent = JSON.parse( promptResult.choices[0].message.tool_calls[0].function.arguments, @@ -61,6 +66,8 @@ export class AiService { folderList: string[], temperature: number, ) { + let elapsedTime: number = 0; + const startTime = new Date(); const promptResult = await this.openai.chat.completions.create( { model: gptVersion, @@ -93,6 +100,16 @@ ${content} maxRetries: 5, }, ); + + elapsedTime = new Date().getTime() - startTime.getTime(); + this.discordProvider.send( + [ + `AI 요약 실행 시간: ${elapsedTime}ms`, + `Input : ${content} / [${folderList.join(', ')}]`, + `Output : ${promptResult} `, + ].join('\n'), + ); + return promptResult; } } diff --git a/src/infrastructure/discord/discord-webhook.provider.ts b/src/infrastructure/discord/discord-webhook.provider.ts new file mode 100644 index 0000000..37cbf60 --- /dev/null +++ b/src/infrastructure/discord/discord-webhook.provider.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class DiscordWebhookProvider { + constructor(private readonly configService: ConfigService) {} + + public async send(content: string) { + const discordWebhook = this.configService.get('DISCORD_WEBHOOK_URL'); + + await fetch(discordWebhook, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }), + }); + } +} diff --git a/src/infrastructure/discord/discord.module.ts b/src/infrastructure/discord/discord.module.ts new file mode 100644 index 0000000..e3dc7c1 --- /dev/null +++ b/src/infrastructure/discord/discord.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { DiscordWebhookProvider } from './discord-webhook.provider'; + +@Module({ + providers: [DiscordWebhookProvider], + exports: [DiscordWebhookProvider], +}) +export class DiscordModule {} From 6ca291ef77c1274586419861b7b44a043716694d Mon Sep 17 00:00:00 2001 From: Geonoing Date: Thu, 18 Jul 2024 02:42:49 +0900 Subject: [PATCH 03/19] chore: add createdAt to post list --- src/modules/posts/response/listPost.response.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/posts/response/listPost.response.ts b/src/modules/posts/response/listPost.response.ts index ede8b74..ee422b2 100644 --- a/src/modules/posts/response/listPost.response.ts +++ b/src/modules/posts/response/listPost.response.ts @@ -27,6 +27,9 @@ export class ListPostItem { }) readAt: Date; + @ApiProperty() + createdAt: Date; + constructor(data: Post & { _id: Types.ObjectId }) { this.id = data._id.toString(); this.folderId = data.folderId.toString(); @@ -35,6 +38,7 @@ export class ListPostItem { this.description = data.description; this.isFavorite = data.isFavorite; this.readAt = data.readAt; + this.createdAt = data.createdAt; } } From 979b5bb411d24605b62584092aefe8883a30d2f2 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Thu, 18 Jul 2024 02:45:50 +0900 Subject: [PATCH 04/19] feat: Add post keywords by ai classification --- src/ai_handler.ts | 44 ++++++++++++++++++-- src/modules/folders/folders.service.ts | 12 +++--- src/modules/posts/postKeywords.repository.ts | 21 ++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/modules/posts/postKeywords.repository.ts diff --git a/src/ai_handler.ts b/src/ai_handler.ts index 5b7ff1d..e06924e 100644 --- a/src/ai_handler.ts +++ b/src/ai_handler.ts @@ -1,16 +1,50 @@ -import { Handler } from 'aws-lambda'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; +import { MongooseModule } from '@nestjs/mongoose'; import { AiService } from '@src/infrastructure/ai/ai.service'; -import { AppModule } from '@src/app.module'; +import { Handler } from 'aws-lambda'; +import { + DatabaseModule, + Folder, + FolderSchema, + Post, + PostSchema, +} from './infrastructure'; +import { AiModule } from './infrastructure/ai/ai.module'; import { LambdaEventPayload } from './infrastructure/aws-lambda/type'; import { ClassficiationRepository } from './modules/classification/classification.repository'; +import { PostKeywordsRepository } from './modules/posts/postKeywords.repository'; import { PostsRepository } from './modules/posts/posts.repository'; +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + envFilePath: `.env.${process.env.NODE_ENV || 'local'}`, + }), + DatabaseModule, + MongooseModule.forFeature([ + { name: Post.name, schema: PostSchema }, + { name: Folder.name, schema: FolderSchema }, + ]), + AiModule, + ], + providers: [ + ClassficiationRepository, + PostsRepository, + PostKeywordsRepository, + ], +}) +class WorkerModule {} + export const handler: Handler = async (event: LambdaEventPayload) => { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(WorkerModule); const aiService = app.get(AiService); const classificationRepository = app.get(ClassficiationRepository); const postRepository = app.get(PostsRepository); + const postKeywordsRepository = app.get(PostKeywordsRepository); // Map - (Folder Name):(Folder ID) const folderMapper = {}; @@ -41,6 +75,10 @@ export const handler: Handler = async (event: LambdaEventPayload) => { classification._id.toString(), summarizeUrlContent.response.summary, ); + await postKeywordsRepository.createPostKeywords( + postId, + summarizeUrlContent.response.keywords, + ); } // NOTE: cloud-watch 로그 확인용 diff --git a/src/modules/folders/folders.service.ts b/src/modules/folders/folders.service.ts index 9b5053f..9b1e595 100644 --- a/src/modules/folders/folders.service.ts +++ b/src/modules/folders/folders.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { CreateFolderDto, UpdateFolderDto } from './dto/mutate-folder.dto'; +import { sum } from '@src/common'; +import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; import { Schema as MongooseSchema } from 'mongoose'; -import { FolderRepository } from './folders.repository'; import { PostsRepository } from '../posts/posts.repository'; -import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; -import { sum } from '@src/common'; import { FolderListServiceDto } from './dto/folder-with-count.dto'; +import { CreateFolderDto, UpdateFolderDto } from './dto/mutate-folder.dto'; +import { FolderRepository } from './folders.repository'; @Injectable() export class FoldersService { @@ -67,7 +67,7 @@ export class FoldersService { userId: new MongooseSchema.Types.ObjectId(userId), postCount: favoritePostCount, }; - const defaultFolderTmp = { + const readLater = { id: defaultFolder.id, name: defaultFolder.name, type: FolderType.DEFAULT, @@ -75,7 +75,7 @@ export class FoldersService { postCount: allPostCount - customFoldersPostCount, }; - const defaultFolders = [all, favorite, defaultFolderTmp].filter( + const defaultFolders = [all, favorite, readLater].filter( (folder) => !!folder, ); return { defaultFolders, customFolders }; diff --git a/src/modules/posts/postKeywords.repository.ts b/src/modules/posts/postKeywords.repository.ts new file mode 100644 index 0000000..0b6a0e3 --- /dev/null +++ b/src/modules/posts/postKeywords.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { PostKeyword } from '@src/infrastructure/database/schema/postKeyword.schema'; +import { Model } from 'mongoose'; + +@Injectable() +export class PostKeywordsRepository { + constructor( + @InjectModel(PostKeyword.name) + private readonly postKeywordModel: Model, + ) {} + + async createPostKeywords(postId: string, keywords: string[]) { + const postKeywords = keywords.map((keyword) => ({ + postId, + keyword, + })); + + await this.postKeywordModel.insertMany(postKeywords); + } +} From 62dcd912c5c00d488a7e97706744ba0505e3fd31 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sat, 20 Jul 2024 17:22:49 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20discord=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_handler.ts | 1 + src/app.config.ts | 7 +++- src/app.module.ts | 14 ++++---- src/common/constant/index.ts | 1 + src/common/filter/base.filter.ts | 32 ++++++++++++++++++- src/domains/folder.ts | 9 ------ src/infrastructure/ai/ai.module.ts | 3 +- src/infrastructure/ai/ai.service.ts | 15 ++++++--- src/infrastructure/aws-lambda/type.ts | 1 + .../discord/discord-ai-webhook.provider.ts | 17 ++++++++++ .../discord/discord-error-webhook.provider.ts | 17 ++++++++++ .../discord/discord-webhook.provider.ts | 13 +++----- src/infrastructure/discord/discord.module.ts | 10 +++--- src/modules/folders/dto/index.ts | 2 ++ .../folders/responses/folder-list.response.ts | 3 +- .../responses/folder-summary.response.ts | 1 + .../folders/responses/post.response.ts | 5 --- src/modules/posts/posts.service.ts | 1 + 18 files changed, 108 insertions(+), 44 deletions(-) create mode 100644 src/common/constant/index.ts delete mode 100644 src/domains/folder.ts create mode 100644 src/infrastructure/discord/discord-ai-webhook.provider.ts create mode 100644 src/infrastructure/discord/discord-error-webhook.provider.ts diff --git a/src/ai_handler.ts b/src/ai_handler.ts index e06924e..ab65209 100644 --- a/src/ai_handler.ts +++ b/src/ai_handler.ts @@ -57,6 +57,7 @@ export const handler: Handler = async (event: LambdaEventPayload) => { const summarizeUrlContent = await aiService.summarizeLinkContent( event.postContent, folderNames, + event.url, ); // NOTE : 요약 성공 시 classification 생성, post 업데이트 diff --git a/src/app.config.ts b/src/app.config.ts index f241512..173ffca 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -6,6 +6,7 @@ import { import { ConfigService } from '@nestjs/config'; import { Reflector } from '@nestjs/core'; import { CommonResponseInterceptor, RootExceptionFilter } from './common'; +import { DiscordErrorWebhookProvider } from './infrastructure/discord/discord-error-webhook.provider'; export async function nestAppConfig< T extends INestApplication = INestApplication, @@ -34,7 +35,11 @@ export function nestResponseConfig< function configFilterStandAlone( app: T, ) { - app.useGlobalFilters(new RootExceptionFilter()); + app.useGlobalFilters( + new RootExceptionFilter( + new DiscordErrorWebhookProvider(new ConfigService()), + ), + ); } // Enalbe Exception Filter with Sentry Connection diff --git a/src/app.module.ts b/src/app.module.ts index fa88a67..03306f4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,19 +2,20 @@ import { Module } from '@nestjs/common'; // Custom Packages -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from '@src/infrastructure'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; import { AiModule } from './infrastructure/ai/ai.module'; -import { UsersModule } from './modules/users/users.module'; -import { ClassificationModule } from './modules/classification/classification.module'; +import { AwsLambdaModule } from './infrastructure/aws-lambda/aws-lambda.module'; +import { DiscordModule } from './infrastructure/discord/discord.module'; import { AuthModule } from './modules/auth/auth.module'; +import { ClassificationModule } from './modules/classification/classification.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'; import { OnboardModule } from './modules/onboard/onboard.module'; +import { PostsModule } from './modules/posts/posts.module'; +import { UsersModule } from './modules/users/users.module'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { OnboardModule } from './modules/onboard/onboard.module'; envFilePath: `.env.${process.env.NODE_ENV || 'local'}`, }), DatabaseModule, + DiscordModule, AiModule, UsersModule, ClassificationModule, diff --git a/src/common/constant/index.ts b/src/common/constant/index.ts new file mode 100644 index 0000000..8842d01 --- /dev/null +++ b/src/common/constant/index.ts @@ -0,0 +1 @@ +export const IS_LOCAL = process.env.NODE_ENV === 'local'; diff --git a/src/common/filter/base.filter.ts b/src/common/filter/base.filter.ts index 4ad3e62..3c1db65 100644 --- a/src/common/filter/base.filter.ts +++ b/src/common/filter/base.filter.ts @@ -5,6 +5,7 @@ import { HttpException, } from '@nestjs/common'; import { captureException } from '@sentry/node'; +import { DiscordErrorWebhookProvider } from '@src/infrastructure/discord/discord-error-webhook.provider'; import { Response } from 'express'; import { RootException, createException } from '../error'; import { ExceptionPayload, ICommonResponse } from '../types/type'; @@ -13,8 +14,13 @@ import { ExceptionPayload, ICommonResponse } from '../types/type'; export class RootExceptionFilter implements ExceptionFilter { private unknownCode = 'Unknown'; - catch(exception: any, host: ArgumentsHost) { + constructor( + private readonly discordErrorWebhookProvider: DiscordErrorWebhookProvider, + ) {} + + async catch(exception: any, host: ArgumentsHost) { const context = host.switchToHttp(); + const request = context.getRequest(); const response: Response = context.getResponse(); let targetException = exception; let responseStatusCode = 500; @@ -65,6 +71,30 @@ export class RootExceptionFilter implements ExceptionFilter { error: responseErrorPayload, }; + await this.handle(request, exception); + return response.status(responseStatusCode).json(exceptionResponse); } + + private async handle(request: Request, error: Error) { + const content = this.parseError(request, error); + + this.discordErrorWebhookProvider.send(content); + } + + private parseError(request: Request, error: Error): string { + return `노드팀 채찍 맞아라~~ 🦹🏿‍♀️👹🦹🏿 +에러 발생 API : ${request.method} ${request.url} + +에러 메세지 : ${error.message} + +에러 위치 : ${error.stack + .split('\n') + .slice(0, 2) + .map((message) => message.trim()) + .join('\n')} + +당장 고쳐서 올렷! + `; + } } diff --git a/src/domains/folder.ts b/src/domains/folder.ts deleted file mode 100644 index ab3cdeb..0000000 --- a/src/domains/folder.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; - -export class FolderDomain { - userId: string; - - name: string; - - type: FolderType; -} diff --git a/src/infrastructure/ai/ai.module.ts b/src/infrastructure/ai/ai.module.ts index 65f0f99..7015877 100644 --- a/src/infrastructure/ai/ai.module.ts +++ b/src/infrastructure/ai/ai.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; -import { DiscordModule } from '../discord/discord.module'; import { AiService } from './ai.service'; @Module({ - imports: [DiscordModule], + imports: [], providers: [AiService], exports: [AiService], }) diff --git a/src/infrastructure/ai/ai.service.ts b/src/infrastructure/ai/ai.service.ts index 9777976..311243a 100644 --- a/src/infrastructure/ai/ai.service.ts +++ b/src/infrastructure/ai/ai.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import OpenAI, { OpenAIError, RateLimitError } from 'openai'; -import { DiscordWebhookProvider } from '../discord/discord-webhook.provider'; +import { DiscordAIWebhookProvider } from '../discord/discord-ai-webhook.provider'; import { gptVersion } from './ai.constant'; import { SummarizeURLContentDto } from './dto'; import { summarizeURLContentFunctionFactory } from './functions'; @@ -13,16 +13,17 @@ export class AiService { constructor( private readonly config: ConfigService, - private readonly discordProvider: DiscordWebhookProvider, + private readonly discordAIWebhookProvider: DiscordAIWebhookProvider, ) { this.openai = new OpenAI({ - apiKey: config.get('OPENAI_API_KEY'), + apiKey: this.config.get('OPENAI_API_KEY'), }); } async summarizeLinkContent( content: string, userFolderList: string[], + url: string, temperature = 0.5, ): Promise { try { @@ -32,6 +33,7 @@ export class AiService { const promptResult = await this.invokeAISummary( content, folderLists, + url, temperature, ); @@ -64,6 +66,7 @@ export class AiService { private async invokeAISummary( content: string, folderList: string[], + url: string, temperature: number, ) { let elapsedTime: number = 0; @@ -102,10 +105,12 @@ ${content} ); elapsedTime = new Date().getTime() - startTime.getTime(); - this.discordProvider.send( + this.discordAIWebhookProvider.send( [ `AI 요약 실행 시간: ${elapsedTime}ms`, - `Input : ${content} / [${folderList.join(', ')}]`, + `Input`, + `- URL : ${url}`, + `- 인풋 폴더 : [${folderList.join(', ')}]`, `Output : ${promptResult} `, ].join('\n'), ); diff --git a/src/infrastructure/aws-lambda/type.ts b/src/infrastructure/aws-lambda/type.ts index 8ab5a76..d6cb21a 100644 --- a/src/infrastructure/aws-lambda/type.ts +++ b/src/infrastructure/aws-lambda/type.ts @@ -2,4 +2,5 @@ export type LambdaEventPayload = { postContent: string; folderList: { id: string; name: string }[]; postId: string; + url: string; }; diff --git a/src/infrastructure/discord/discord-ai-webhook.provider.ts b/src/infrastructure/discord/discord-ai-webhook.provider.ts new file mode 100644 index 0000000..3021c63 --- /dev/null +++ b/src/infrastructure/discord/discord-ai-webhook.provider.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DiscordWebhookProvider } from './discord-webhook.provider'; + +@Injectable() +export class DiscordAIWebhookProvider extends DiscordWebhookProvider { + protected readonly webhookUrl: string; + + constructor(private readonly configService: ConfigService) { + super(); + this.webhookUrl = this.configService.get('DISCORD_AI_WEBHOOK_URL'); + } + + public async send(content: string) { + await super.send(this.webhookUrl, content); + } +} diff --git a/src/infrastructure/discord/discord-error-webhook.provider.ts b/src/infrastructure/discord/discord-error-webhook.provider.ts new file mode 100644 index 0000000..e6cd208 --- /dev/null +++ b/src/infrastructure/discord/discord-error-webhook.provider.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DiscordWebhookProvider } from './discord-webhook.provider'; + +@Injectable() +export class DiscordErrorWebhookProvider extends DiscordWebhookProvider { + protected webhookUrl: string; + + constructor(private readonly configService: ConfigService) { + super(); + this.webhookUrl = this.configService.get('DISCORD_ERROR_WEBHOOK_URL'); + } + + public async send(content: string) { + await super.send(this.webhookUrl, content); + } +} diff --git a/src/infrastructure/discord/discord-webhook.provider.ts b/src/infrastructure/discord/discord-webhook.provider.ts index 37cbf60..5c32036 100644 --- a/src/infrastructure/discord/discord-webhook.provider.ts +++ b/src/infrastructure/discord/discord-webhook.provider.ts @@ -1,14 +1,9 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() export class DiscordWebhookProvider { - constructor(private readonly configService: ConfigService) {} - - public async send(content: string) { - const discordWebhook = this.configService.get('DISCORD_WEBHOOK_URL'); + protected readonly webhookUrl: string; + constructor() {} - await fetch(discordWebhook, { + public async send(url: string, content: string) { + await fetch(url, { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }), diff --git a/src/infrastructure/discord/discord.module.ts b/src/infrastructure/discord/discord.module.ts index e3dc7c1..01291d2 100644 --- a/src/infrastructure/discord/discord.module.ts +++ b/src/infrastructure/discord/discord.module.ts @@ -1,8 +1,10 @@ -import { Module } from '@nestjs/common'; -import { DiscordWebhookProvider } from './discord-webhook.provider'; +import { Global, Module } from '@nestjs/common'; +import { DiscordAIWebhookProvider } from './discord-ai-webhook.provider'; +import { DiscordErrorWebhookProvider } from './discord-error-webhook.provider'; +@Global() @Module({ - providers: [DiscordWebhookProvider], - exports: [DiscordWebhookProvider], + providers: [DiscordAIWebhookProvider, DiscordErrorWebhookProvider], + exports: [DiscordAIWebhookProvider, DiscordErrorWebhookProvider], }) export class DiscordModule {} diff --git a/src/modules/folders/dto/index.ts b/src/modules/folders/dto/index.ts index c40fbc4..c4edb07 100644 --- a/src/modules/folders/dto/index.ts +++ b/src/modules/folders/dto/index.ts @@ -1 +1,3 @@ +export * from './delete-custom-folder.dto'; +export * from './folder-list-service.dto'; export * from './mutate-folder.dto'; diff --git a/src/modules/folders/responses/folder-list.response.ts b/src/modules/folders/responses/folder-list.response.ts index ced75c9..528fa9e 100644 --- a/src/modules/folders/responses/folder-list.response.ts +++ b/src/modules/folders/responses/folder-list.response.ts @@ -1,7 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; +import { FolderListServiceDto } from '../dto/folder-list-service.dto'; import { FolderResponse } from './folder.response'; -import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; -import { FolderListServiceDto } from '../dto/folder-with-count.dto'; export class FolderListResponse { @ApiProperty({ isArray: true, type: FolderResponse }) diff --git a/src/modules/folders/responses/folder-summary.response.ts b/src/modules/folders/responses/folder-summary.response.ts index 075ae3e..7b07623 100644 --- a/src/modules/folders/responses/folder-summary.response.ts +++ b/src/modules/folders/responses/folder-summary.response.ts @@ -16,6 +16,7 @@ export class FolderSummaryResponse { createdAt: Date; constructor(data: FolderDocument) { + /** @todo postgres로 바꾸면서 수정하기 */ this.id = data._id ? data._id.toString() : data.id; this.name = data.name; this.type = data.type; diff --git a/src/modules/folders/responses/post.response.ts b/src/modules/folders/responses/post.response.ts index 432d054..d5d5235 100644 --- a/src/modules/folders/responses/post.response.ts +++ b/src/modules/folders/responses/post.response.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger'; -import { FolderResponse } from './folder.response'; import { PostDocument } from '@src/infrastructure'; /** @@ -22,9 +21,6 @@ export class PostResponse { @ApiProperty() id: string; - @ApiProperty() - userId: string; - @ApiProperty() folderId: string; @@ -48,7 +44,6 @@ export class PostResponse { constructor(data: PostDocument) { this.id = data._id.toString(); - this.userId = data.userId.toString(); this.folderId = data.folderId.toString(); this.url = data.url; this.title = data.title; diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index fe370b0..5316c8d 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -59,6 +59,7 @@ export class PostsService { title, ); const payload = { + url: createPostDto.url, postContent: content, folderList: folders, postId: postId, From 2fb40c15d6fdb66164f94df1c13b0262ff96a6d6 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sat, 20 Jul 2024 17:28:53 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EB=B0=8F=20post=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../folders/dto/delete-custom-folder.dto.ts | 11 +++++++ .../folders/dto/folder-list-service.dto.ts | 5 ++++ .../folders/dto/folder-with-count.dto.ts | 8 ----- src/modules/folders/folders.controller.ts | 30 ++++++++++++------- src/modules/folders/folders.repository.ts | 9 +++++- src/modules/folders/folders.service.ts | 6 +++- src/modules/posts/posts.repository.ts | 5 ++++ src/modules/posts/posts.service.ts | 12 ++++++++ 8 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 src/modules/folders/dto/delete-custom-folder.dto.ts create mode 100644 src/modules/folders/dto/folder-list-service.dto.ts delete mode 100644 src/modules/folders/dto/folder-with-count.dto.ts diff --git a/src/modules/folders/dto/delete-custom-folder.dto.ts b/src/modules/folders/dto/delete-custom-folder.dto.ts new file mode 100644 index 0000000..22ad08a --- /dev/null +++ b/src/modules/folders/dto/delete-custom-folder.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteCustomFolderDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: '삭제할 유저 id', + }) + userId: string; +} diff --git a/src/modules/folders/dto/folder-list-service.dto.ts b/src/modules/folders/dto/folder-list-service.dto.ts new file mode 100644 index 0000000..7b25171 --- /dev/null +++ b/src/modules/folders/dto/folder-list-service.dto.ts @@ -0,0 +1,5 @@ +export interface FolderListServiceDto { + /** */ + defaultFolders: any[]; + customFolders: any[]; +} diff --git a/src/modules/folders/dto/folder-with-count.dto.ts b/src/modules/folders/dto/folder-with-count.dto.ts deleted file mode 100644 index f44622c..0000000 --- a/src/modules/folders/dto/folder-with-count.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FolderDomain } from '@src/domains/folder'; -import { FolderDocument } from '@src/infrastructure'; - -export interface FolderListServiceDto { - /** */ - defaultFolders: any[]; - customFolders: any[]; -} diff --git a/src/modules/folders/folders.controller.ts b/src/modules/folders/folders.controller.ts index 61c14ad..97676e9 100644 --- a/src/modules/folders/folders.controller.ts +++ b/src/modules/folders/folders.controller.ts @@ -1,17 +1,18 @@ import { + Body, Controller, + Delete, Get, - Post, - Body, - Patch, Param, - Delete, - UseGuards, + Patch, + Post, Query, + UseGuards, } from '@nestjs/common'; -import { FoldersService } from './folders.service'; -import { CreateFolderDto, UpdateFolderDto } from './dto'; import { GetUser } from '@src/common'; +import { GetPostQueryDto } from '../posts/dto/find-in-folder.dto'; +import { PostsService } from '../posts/posts.service'; +import { JwtGuard } from '../users/guards'; import { CreateFolderDocs, DeleteFolderDocs, @@ -21,14 +22,13 @@ import { FolderControllerDocs, UpdateFolderDocs, } from './docs'; +import { CreateFolderDto, DeleteCustomFolderDto, UpdateFolderDto } from './dto'; +import { FoldersService } from './folders.service'; import { FolderListResponse, FolderSummaryResponse, PostListInFolderResponse, } from './responses'; -import { JwtGuard } from '../users/guards'; -import { PostsService } from '../posts/posts.service'; -import { GetPostQueryDto } from '../posts/dto/find-in-folder.dto'; @FolderControllerDocs @UseGuards(JwtGuard) @@ -96,6 +96,16 @@ export class FoldersController { await this.foldersService.update(userId, folderId, updateFolderDto); } + @Delete('/all') + async removeAll(@Query() deleteCustomFolderDto: DeleteCustomFolderDto) { + await this.postsService.removeAllPostsInCustomFolders( + deleteCustomFolderDto.userId, + ); + await this.foldersService.removeAllCustomFolders( + deleteCustomFolderDto.userId, + ); + } + @DeleteFolderDocs @Delete(':folderId') async remove(@GetUser() userId: string, @Param('folderId') folderId: string) { diff --git a/src/modules/folders/folders.repository.ts b/src/modules/folders/folders.repository.ts index 1fe7666..eed105a 100644 --- a/src/modules/folders/folders.repository.ts +++ b/src/modules/folders/folders.repository.ts @@ -1,8 +1,8 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { FilterQuery, Model } from 'mongoose'; import { Folder, FolderDocument } from '@src/infrastructure'; import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; +import { FilterQuery, Model } from 'mongoose'; @Injectable() export class FolderRepository { @@ -40,4 +40,11 @@ export class FolderRepository { return folder; } + + async deleteAllCustomFolder(userId: string) { + await this.folderModel.deleteMany({ + userId, + type: FolderType.CUSTOM, + }); + } } diff --git a/src/modules/folders/folders.service.ts b/src/modules/folders/folders.service.ts index 9b1e595..cb690e4 100644 --- a/src/modules/folders/folders.service.ts +++ b/src/modules/folders/folders.service.ts @@ -3,7 +3,7 @@ import { sum } from '@src/common'; import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; import { Schema as MongooseSchema } from 'mongoose'; import { PostsRepository } from '../posts/posts.repository'; -import { FolderListServiceDto } from './dto/folder-with-count.dto'; +import { FolderListServiceDto } from './dto/folder-list-service.dto'; import { CreateFolderDto, UpdateFolderDto } from './dto/mutate-folder.dto'; import { FolderRepository } from './folders.repository'; @@ -112,4 +112,8 @@ export class FoldersService { await folder.deleteOne().exec(); } + + async removeAllCustomFolders(userId: string) { + await this.folderRepository.deleteAllCustomFolder(userId); + } } diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts index 16a4a42..18390f8 100644 --- a/src/modules/posts/posts.repository.ts +++ b/src/modules/posts/posts.repository.ts @@ -381,4 +381,9 @@ export class PostsRepository { } return deleteResult; } + + async deleteMany(param: FilterQuery) { + await this.postModel.deleteMany(param); + 1; + } } diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index 5316c8d..aa18116 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -128,4 +128,16 @@ export class PostsService { ); return true; } + + async removeAllPostsInCustomFolders(userId: string) { + const customFolders = await this.folderRepository.findByUserId(userId); + const customFolderIds = customFolders + .filter((folder) => folder.type === FolderType.CUSTOM) + .map((folder) => folder._id); + + await this.postRepository.deleteMany({ + userId, + folderId: { $in: customFolderIds }, + }); + } } From 442fde6c24e6b75b1210e53b5fe4de73ad275fc8 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:00:22 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20AI=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=94=EC=B6=9C=20keyword=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_handler.ts | 77 +++--------------- src/infrastructure/ai/ai.service.ts | 22 ++++- src/infrastructure/ai/functions/index.ts | 6 ++ .../aws-lambda/aws-lambda.service.ts | 6 +- src/infrastructure/aws-lambda/type.ts | 3 +- .../ai-classification.module.ts | 46 +++++++++++ .../ai-classification.service.ts | 80 +++++++++++++++++++ src/modules/folders/folders.repository.ts | 9 +++ src/modules/keywords/keyword.repository.ts | 20 +++++ src/modules/posts/posts.module.ts | 4 +- src/modules/posts/posts.service.ts | 42 +++++++--- 11 files changed, 229 insertions(+), 86 deletions(-) create mode 100644 src/modules/ai-classification/ai-classification.module.ts create mode 100644 src/modules/ai-classification/ai-classification.service.ts create mode 100644 src/modules/keywords/keyword.repository.ts diff --git a/src/ai_handler.ts b/src/ai_handler.ts index ab65209..8a30f2e 100644 --- a/src/ai_handler.ts +++ b/src/ai_handler.ts @@ -1,21 +1,11 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; -import { MongooseModule } from '@nestjs/mongoose'; -import { AiService } from '@src/infrastructure/ai/ai.service'; import { Handler } from 'aws-lambda'; -import { - DatabaseModule, - Folder, - FolderSchema, - Post, - PostSchema, -} from './infrastructure'; -import { AiModule } from './infrastructure/ai/ai.module'; -import { LambdaEventPayload } from './infrastructure/aws-lambda/type'; -import { ClassficiationRepository } from './modules/classification/classification.repository'; -import { PostKeywordsRepository } from './modules/posts/postKeywords.repository'; -import { PostsRepository } from './modules/posts/posts.repository'; +import { DatabaseModule } from './infrastructure'; +import { AiClassificationPayload } from './infrastructure/aws-lambda/type'; +import { AiClassificationModule } from './modules/ai-classification/ai-classification.module'; +import { AiClassificationService } from './modules/ai-classification/ai-classification.service'; @Module({ imports: [ @@ -25,67 +15,22 @@ import { PostsRepository } from './modules/posts/posts.repository'; envFilePath: `.env.${process.env.NODE_ENV || 'local'}`, }), DatabaseModule, - MongooseModule.forFeature([ - { name: Post.name, schema: PostSchema }, - { name: Folder.name, schema: FolderSchema }, - ]), - AiModule, - ], - providers: [ - ClassficiationRepository, - PostsRepository, - PostKeywordsRepository, + AiClassificationModule, ], + providers: [AiClassificationService], }) class WorkerModule {} -export const handler: Handler = async (event: LambdaEventPayload) => { +export const handler: Handler = async (event: AiClassificationPayload) => { const app = await NestFactory.create(WorkerModule); - const aiService = app.get(AiService); - const classificationRepository = app.get(ClassficiationRepository); - const postRepository = app.get(PostsRepository); - const postKeywordsRepository = app.get(PostKeywordsRepository); - - // Map - (Folder Name):(Folder ID) - const folderMapper = {}; - const folderNames = event.folderList.map((folder) => { - folderMapper[folder.name] = folder.id; - return folder.name; - }); - - // NOTE: AI 요약 요청 - const summarizeUrlContent = await aiService.summarizeLinkContent( - event.postContent, - folderNames, - event.url, - ); + const aiClassificationService = app.get(AiClassificationService); - // NOTE : 요약 성공 시 classification 생성, post 업데이트 - if (summarizeUrlContent.success) { - const postId = event.postId; - const folderId = folderMapper[summarizeUrlContent.response.category]; - const post = await postRepository.findPostByIdForAIClassification(postId); - const classification = await classificationRepository.createClassification( - post.url, - summarizeUrlContent.response.summary, - summarizeUrlContent.response.keywords, - folderId, - ); - await postRepository.updatePostClassificationForAIClassification( - postId, - classification._id.toString(), - summarizeUrlContent.response.summary, - ); - await postKeywordsRepository.createPostKeywords( - postId, - summarizeUrlContent.response.keywords, - ); - } + const result = await aiClassificationService.execute(event); // NOTE: cloud-watch 로그 확인용 console.log({ - result: summarizeUrlContent.success ? 'success' : 'fail', + result: result.success ? 'success' : 'fail', event: JSON.stringify(event, null, 2), - summarizeUrlContent: summarizeUrlContent, + summarizeUrlContent: result, }); }; diff --git a/src/infrastructure/ai/ai.service.ts b/src/infrastructure/ai/ai.service.ts index 311243a..d65f3f8 100644 --- a/src/infrastructure/ai/ai.service.ts +++ b/src/infrastructure/ai/ai.service.ts @@ -4,7 +4,10 @@ import OpenAI, { OpenAIError, RateLimitError } from 'openai'; import { DiscordAIWebhookProvider } from '../discord/discord-ai-webhook.provider'; import { gptVersion } from './ai.constant'; import { SummarizeURLContentDto } from './dto'; -import { summarizeURLContentFunctionFactory } from './functions'; +import { + AiClassificationFunctionResult, + summarizeURLContentFunctionFactory, +} from './functions'; import { SummarizeURLContent } from './types/types'; @Injectable() @@ -105,13 +108,24 @@ ${content} ); elapsedTime = new Date().getTime() - startTime.getTime(); + + const functionResult: AiClassificationFunctionResult = JSON.parse( + promptResult.choices[0].message.tool_calls[0].function.arguments, + ); this.discordAIWebhookProvider.send( [ - `AI 요약 실행 시간: ${elapsedTime}ms`, - `Input`, + `**AI 요약 실행 시간: ${elapsedTime}ms**`, + `**Input**`, `- URL : ${url}`, `- 인풋 폴더 : [${folderList.join(', ')}]`, - `Output : ${promptResult} `, + `**Output**`, + `- 사용 모델 : ${promptResult.model}`, + `- 요약 : ${functionResult.summary}`, + `- 추출 키워드 : ${functionResult.keywords.join(', ')}`, + `- 매칭 폴더명 : ${functionResult.category}`, + `- Input Token : ${promptResult.usage.prompt_tokens}`, + `- Output Token : ${promptResult.usage.completion_tokens}`, + `- Total Token : ${promptResult.usage.total_tokens}`, ].join('\n'), ); diff --git a/src/infrastructure/ai/functions/index.ts b/src/infrastructure/ai/functions/index.ts index e83f675..3542444 100644 --- a/src/infrastructure/ai/functions/index.ts +++ b/src/infrastructure/ai/functions/index.ts @@ -1,3 +1,9 @@ +export type AiClassificationFunctionResult = { + summary: string; + keywords: string[]; + category: string; +}; + export function summarizeURLContentFunctionFactory(folderList: string[]) { return { name: 'summarizeURL', diff --git a/src/infrastructure/aws-lambda/aws-lambda.service.ts b/src/infrastructure/aws-lambda/aws-lambda.service.ts index 16de8a5..b8062e2 100644 --- a/src/infrastructure/aws-lambda/aws-lambda.service.ts +++ b/src/infrastructure/aws-lambda/aws-lambda.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@nestjs/common'; import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { LambdaEventPayload } from './type'; +import { AiClassificationPayload } from './type'; @Injectable() export class AwsLambdaService { @@ -16,7 +16,7 @@ export class AwsLambdaService { async invokeLambda( lambdaFunctionName: string, - payload: LambdaEventPayload, + payload: AiClassificationPayload, ): Promise { const command = new InvokeCommand({ FunctionName: lambdaFunctionName, diff --git a/src/infrastructure/aws-lambda/type.ts b/src/infrastructure/aws-lambda/type.ts index d6cb21a..0bd63a4 100644 --- a/src/infrastructure/aws-lambda/type.ts +++ b/src/infrastructure/aws-lambda/type.ts @@ -1,6 +1,7 @@ -export type LambdaEventPayload = { +export type AiClassificationPayload = { postContent: string; folderList: { id: string; name: string }[]; + userId: string; postId: string; url: string; }; diff --git a/src/modules/ai-classification/ai-classification.module.ts b/src/modules/ai-classification/ai-classification.module.ts new file mode 100644 index 0000000..59149d0 --- /dev/null +++ b/src/modules/ai-classification/ai-classification.module.ts @@ -0,0 +1,46 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { + AIClassification, + AIClassificationSchema, + Folder, + FolderSchema, + Keyword, + KeywordSchema, + Post, + PostSchema, +} from '@src/infrastructure'; +import { AiModule } from '@src/infrastructure/ai/ai.module'; +import { + PostKeyword, + PostKeywordSchema, +} from '@src/infrastructure/database/schema/postKeyword.schema'; +import { ClassficiationRepository } from '../classification/classification.repository'; +import { FolderRepository } from '../folders/folders.repository'; +import { KeywordsRepository } from '../keywords/keyword.repository'; +import { PostKeywordsRepository } from '../posts/postKeywords.repository'; +import { PostsRepository } from '../posts/posts.repository'; +import { AiClassificationService } from './ai-classification.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: Post.name, schema: PostSchema }, + { name: Folder.name, schema: FolderSchema }, + { name: Keyword.name, schema: KeywordSchema }, + { name: PostKeyword.name, schema: PostKeywordSchema }, + { name: AIClassification.name, schema: AIClassificationSchema }, + ]), + AiModule, + ], + providers: [ + AiClassificationService, + ClassficiationRepository, + FolderRepository, + PostsRepository, + KeywordsRepository, + PostKeywordsRepository, + ], + exports: [AiClassificationService], +}) +export class AiClassificationModule {} diff --git a/src/modules/ai-classification/ai-classification.service.ts b/src/modules/ai-classification/ai-classification.service.ts new file mode 100644 index 0000000..f80fadb --- /dev/null +++ b/src/modules/ai-classification/ai-classification.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { AiService } from '@src/infrastructure/ai/ai.service'; +import { AiClassificationPayload } from '@src/infrastructure/aws-lambda/type'; +import { ClassficiationRepository } from '../classification/classification.repository'; +import { FolderRepository } from '../folders/folders.repository'; +import { KeywordsRepository } from '../keywords/keyword.repository'; +import { PostKeywordsRepository } from '../posts/postKeywords.repository'; +import { PostsRepository } from '../posts/posts.repository'; + +@Injectable() +export class AiClassificationService { + constructor( + private readonly aiService: AiService, + private readonly classificationRepository: ClassficiationRepository, + private readonly folderRepository: FolderRepository, + private readonly postRepository: PostsRepository, + private readonly keywordsRepository: KeywordsRepository, + private readonly postKeywordsRepository: PostKeywordsRepository, + ) {} + + async execute(payload: AiClassificationPayload) { + try { + // Map - (Folder Name):(Folder ID) + + const folderMapper = {}; + const folderNames = payload.folderList.map((folder) => { + folderMapper[folder.name] = folder.id; + return folder.name; + }); + + // NOTE: AI 요약 요청 + const summarizeUrlContent = await this.aiService.summarizeLinkContent( + payload.postContent, + folderNames, + payload.url, + ); + + // NOTE : 요약 성공 시 classification 생성, post 업데이트 + if (summarizeUrlContent.success) { + const postId = payload.postId; + let folderId = folderMapper[summarizeUrlContent.response.category]; + + if (!folderId) { + folderId = await this.folderRepository.getDefaultFolder( + payload.userId, + ); + } + + const post = + await this.postRepository.findPostByIdForAIClassification(postId); + const classification = + await this.classificationRepository.createClassification( + post.url, + summarizeUrlContent.response.summary, + summarizeUrlContent.response.keywords, + folderId, + ); + await this.postRepository.updatePostClassificationForAIClassification( + postId, + classification._id.toString(), + summarizeUrlContent.response.summary, + ); + + const keywords = await this.keywordsRepository.createMany( + summarizeUrlContent.response.keywords, + ); + + const keywordIds = keywords.map((keyword) => keyword._id.toString()); + await this.postKeywordsRepository.createPostKeywords( + postId, + keywordIds, + ); + } + + return summarizeUrlContent; + } catch (error: unknown) { + return { success: fail, error }; + } + } +} diff --git a/src/modules/folders/folders.repository.ts b/src/modules/folders/folders.repository.ts index eed105a..f1abb9c 100644 --- a/src/modules/folders/folders.repository.ts +++ b/src/modules/folders/folders.repository.ts @@ -47,4 +47,13 @@ export class FolderRepository { type: FolderType.CUSTOM, }); } + + async getDefaultFolder(userId: string) { + const folder = await this.folderModel.findOne({ + userId, + type: FolderType.DEFAULT, + }); + + return folder; + } } diff --git a/src/modules/keywords/keyword.repository.ts b/src/modules/keywords/keyword.repository.ts new file mode 100644 index 0000000..ff14a5a --- /dev/null +++ b/src/modules/keywords/keyword.repository.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Keyword } from '@src/infrastructure'; +import { Model } from 'mongoose'; + +@Injectable() +export class KeywordsRepository { + constructor( + @InjectModel(Keyword.name) + private readonly keywordModel: Model, + ) {} + + async createMany(keywords: string[]) { + return await this.keywordModel.insertMany( + keywords.map((keyword) => ({ + name: keyword, + })), + ); + } +} diff --git a/src/modules/posts/posts.module.ts b/src/modules/posts/posts.module.ts index e1e5b8d..31c7270 100644 --- a/src/modules/posts/posts.module.ts +++ b/src/modules/posts/posts.module.ts @@ -21,12 +21,12 @@ import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.serv MongooseModule.forFeature([ { name: Post.name, schema: PostSchema }, { name: Folder.name, schema: FolderSchema }, - ]), - MongooseModule.forFeature([ { name: AIClassification.name, schema: AIClassificationSchema }, + { name: PostKeyword.name, schema: PostKeywordSchema }, ]), UsersModule, AwsLambdaModule, + AiClassificationModule, ], controllers: [PostsController], providers: [ diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index aa18116..9dfb439 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -1,19 +1,28 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { parseLinkTitleAndContent } from '@src/common'; +import { IS_LOCAL } from '@src/common/constant'; +import { Keyword } from '@src/infrastructure'; +import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.service'; +import { AiClassificationPayload } from '@src/infrastructure/aws-lambda/type'; +import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; import { CreatePostDto } from '@src/modules/posts/dto/create-post.dto'; import { PostsRepository } from '@src/modules/posts/posts.repository'; -import { GetPostQueryDto } from './dto/find-in-folder.dto'; +import { AiClassificationService } from '../ai-classification/ai-classification.service'; import { FolderRepository } from '../folders/folders.repository'; import { ListPostQueryDto, UpdatePostDto, UpdatePostFolderDto } from './dto'; -import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.service'; -import { parseLinkTitleAndContent } from '@src/common'; -import { ConfigService } from '@nestjs/config'; +import { GetPostQueryDto } from './dto/find-in-folder.dto'; +import { PostKeywordsRepository } from './postKeywords.repository'; @Injectable() export class PostsService { constructor( private readonly postRepository: PostsRepository, private readonly folderRepository: FolderRepository, + private readonly postKeywordsRepository: PostKeywordsRepository, + private readonly awsLambdaService: AwsLambdaService, + private readonly aiClassificationService: AiClassificationService, private readonly config: ConfigService, ) {} @@ -49,9 +58,7 @@ export class PostsService { name: folder.name, }; }); - const aiLambdaFunctionName = this.config.get( - 'LAMBDA_FUNCTION_NAME', - ); + const postId = await this.postRepository.createPost( userId, createPostDto.folderId, @@ -62,9 +69,12 @@ export class PostsService { url: createPostDto.url, postContent: content, folderList: folders, - postId: postId, - }; - await this.awsLambdaService.invokeLambda(aiLambdaFunctionName, payload); + postId, + userId, + } satisfies AiClassificationPayload; + + await this.executeAiClassification(payload); + return true; } @@ -140,4 +150,16 @@ export class PostsService { folderId: { $in: customFolderIds }, }); } + + private async executeAiClassification(payload: AiClassificationPayload) { + if (IS_LOCAL) { + return await this.aiClassificationService.execute(payload); + } + + const aiLambdaFunctionName = this.config.get( + 'LAMBDA_FUNCTION_NAME', + ); + + await this.awsLambdaService.invokeLambda(aiLambdaFunctionName, payload); + } } From 48ee0f15a2bc45c77236114289a18e011e55fec8 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:00:40 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20post=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20keyword=20=EB=8F=84=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/posts/postKeywords.repository.ts | 13 +++++++--- src/modules/posts/posts.controller.ts | 19 ++++++++------- src/modules/posts/posts.module.ts | 17 +++++++++---- src/modules/posts/posts.repository.ts | 4 ++-- src/modules/posts/posts.service.ts | 24 ++++++++++++++++++- .../posts/response/keyword-list.response.ts | 16 +++++++++++++ .../posts/response/listPost.response.ts | 14 +++++++++-- 7 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 src/modules/posts/response/keyword-list.response.ts diff --git a/src/modules/posts/postKeywords.repository.ts b/src/modules/posts/postKeywords.repository.ts index 0b6a0e3..b87c1b9 100644 --- a/src/modules/posts/postKeywords.repository.ts +++ b/src/modules/posts/postKeywords.repository.ts @@ -10,12 +10,19 @@ export class PostKeywordsRepository { private readonly postKeywordModel: Model, ) {} - async createPostKeywords(postId: string, keywords: string[]) { - const postKeywords = keywords.map((keyword) => ({ + async createPostKeywords(postId: string, keywordIds: string[]) { + const postKeywords = keywordIds.map((keywordId) => ({ postId, - keyword, + keywordId, })); await this.postKeywordModel.insertMany(postKeywords); } + + async findKeywordsByPostIds(postIds: string[]) { + return await this.postKeywordModel + .find({ postId: { $in: postIds } }) + .populate({ path: 'keywordId', model: 'Keyword' }) + .lean(); + } } diff --git a/src/modules/posts/posts.controller.ts b/src/modules/posts/posts.controller.ts index b8da122..9477fab 100644 --- a/src/modules/posts/posts.controller.ts +++ b/src/modules/posts/posts.controller.ts @@ -1,19 +1,18 @@ import { - Controller, - Post, Body, - UseGuards, - Patch, - Param, + Controller, + Delete, Get, + Param, + Patch, + Post, Query, - Delete, + UseGuards, } from '@nestjs/common'; -import { PostsService } from '@src/modules/posts/posts.service'; -import { CreatePostDto } from '@src/modules/posts/dto/create-post.dto'; import { GetUser, PaginationMetadata } from '@src/common'; +import { CreatePostDto } from '@src/modules/posts/dto/create-post.dto'; +import { PostsService } from '@src/modules/posts/posts.service'; import { JwtGuard } from '@src/modules/users/guards'; -import { ListPostQueryDto, UpdatePostDto, UpdatePostFolderDto } from './dto'; import { CreatePostDocs, DeletePostDocs, @@ -21,6 +20,7 @@ import { PostControllerDocs, UpdatePostFolderDocs, } from './docs'; +import { ListPostQueryDto, UpdatePostDto, UpdatePostFolderDto } from './dto'; import { ListPostItem, ListPostResponse } from './response'; @Controller('posts') @@ -35,6 +35,7 @@ export class PostsController { const { count, posts } = await this.postsService.listPost(userId, query); const postResponse = posts.map((post) => new ListPostItem(post)); const metadata = new PaginationMetadata(query.page, query.limit, count); + return new ListPostResponse(metadata, postResponse); } diff --git a/src/modules/posts/posts.module.ts b/src/modules/posts/posts.module.ts index 31c7270..9477a89 100644 --- a/src/modules/posts/posts.module.ts +++ b/src/modules/posts/posts.module.ts @@ -1,7 +1,4 @@ import { Module } from '@nestjs/common'; -import { PostsService } from './posts.service'; -import { PostsController } from './posts.controller'; -import { PostsRepository } from '@src/modules/posts/posts.repository'; import { MongooseModule } from '@nestjs/mongoose'; import { AIClassification, @@ -11,10 +8,19 @@ import { Post, PostSchema, } from '@src/infrastructure'; -import { UsersModule } from '@src/modules/users/users.module'; -import { FolderRepository } from '../folders/folders.repository'; import { AwsLambdaModule } from '@src/infrastructure/aws-lambda/aws-lambda.module'; import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.service'; +import { + PostKeyword, + PostKeywordSchema, +} from '@src/infrastructure/database/schema/postKeyword.schema'; +import { PostsRepository } from '@src/modules/posts/posts.repository'; +import { UsersModule } from '@src/modules/users/users.module'; +import { AiClassificationModule } from '../ai-classification/ai-classification.module'; +import { FolderRepository } from '../folders/folders.repository'; +import { PostKeywordsRepository } from './postKeywords.repository'; +import { PostsController } from './posts.controller'; +import { PostsService } from './posts.service'; @Module({ imports: [ @@ -34,6 +40,7 @@ import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.serv PostsRepository, FolderRepository, AwsLambdaService, + PostKeywordsRepository, ], exports: [PostsService, PostsRepository], }) diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts index 18390f8..f7680b3 100644 --- a/src/modules/posts/posts.repository.ts +++ b/src/modules/posts/posts.repository.ts @@ -4,13 +4,13 @@ import { NotFoundException, } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; +import { OrderType } from '@src/common'; +import { AIClassification, Post, PostDocument } from '@src/infrastructure'; import { FilterQuery, Model, Types } from 'mongoose'; -import { AIClassification, Post } from '@src/infrastructure'; import { ClassificationPostList, PostListInClassificationFolder, } from '../classification/dto/classification.dto'; -import { OrderType } from '@src/common'; import { PostUpdateableFields } from './type/type'; @Injectable() diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index 9dfb439..c433011 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -37,9 +37,31 @@ export class PostsService { query.order, ), ]); + + const postIds = posts.map((post) => post._id.toString()); + const postKeywords = + await this.postKeywordsRepository.findKeywordsByPostIds(postIds); + const postKeywordMap: Record = {}; + + postKeywords.forEach((postKeyword) => { + const postId = postKeyword.postId.toString(); + if (!postKeywordMap[postId]) { + postKeywordMap[postId] = []; + } + + /** + * populate때문에 강제형변환 + */ + const keyword = postKeyword.keywordId as any as Keyword; + postKeywordMap[postId].push(keyword); + }); + return { count, - posts, + posts: posts.map((post) => ({ + ...post, + keywords: postKeywordMap[post._id.toString()] ?? [], + })), }; } diff --git a/src/modules/posts/response/keyword-list.response.ts b/src/modules/posts/response/keyword-list.response.ts new file mode 100644 index 0000000..004fb1e --- /dev/null +++ b/src/modules/posts/response/keyword-list.response.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Keyword } from '@src/infrastructure'; +import { Types } from 'mongoose'; + +export class KeywordItem { + @ApiProperty({ description: '키워드 id' }) + id: string; + + @ApiProperty({ description: '키워드 이름' }) + name: string; + + constructor(keyword: Keyword & { _id: Types.ObjectId }) { + this.id = keyword._id.toString(); + this.name = keyword.name; + } +} diff --git a/src/modules/posts/response/listPost.response.ts b/src/modules/posts/response/listPost.response.ts index ee422b2..aa066de 100644 --- a/src/modules/posts/response/listPost.response.ts +++ b/src/modules/posts/response/listPost.response.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { PaginationMetadata } from '@src/common'; -import { Post } from '@src/infrastructure'; +import { Keyword, Post } from '@src/infrastructure'; import { Types } from 'mongoose'; +import { KeywordItem } from './keyword-list.response'; export class ListPostItem { @ApiProperty() @@ -19,6 +20,9 @@ export class ListPostItem { @ApiProperty() description: string; + @ApiProperty() + keywords: KeywordItem[]; + @ApiProperty() isFavorite: boolean; @@ -30,12 +34,18 @@ export class ListPostItem { @ApiProperty() createdAt: Date; - constructor(data: Post & { _id: Types.ObjectId }) { + constructor( + data: Post & { + _id: Types.ObjectId; + keywords: (Keyword & { _id: Types.ObjectId })[]; + }, + ) { this.id = data._id.toString(); this.folderId = data.folderId.toString(); this.url = data.url; this.title = data.title; this.description = data.description; + this.keywords = data.keywords.map((keyword) => new KeywordItem(keyword)); this.isFavorite = data.isFavorite; this.readAt = data.readAt; this.createdAt = data.createdAt; From 1395eb12d04d2f205f8822db55691ec81c9d67b8 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:27:49 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20post=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=EC=8B=9C=20keyword=EB=B6=99=EC=9D=B4=EA=B8=B0=20=ED=8E=B8?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/filter/base.filter.ts | 2 + .../classification/classification.service.ts | 11 +-- src/modules/folders/docs/folder-api.docs.ts | 3 +- .../responses/post-list-in-folder.response.ts | 11 +-- .../folders/responses/post.response.ts | 23 ++++--- src/modules/posts/posts.repository.ts | 3 +- src/modules/posts/posts.service.ts | 67 ++++++++++++------- 7 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/common/filter/base.filter.ts b/src/common/filter/base.filter.ts index 3c1db65..0aa3d86 100644 --- a/src/common/filter/base.filter.ts +++ b/src/common/filter/base.filter.ts @@ -22,6 +22,8 @@ export class RootExceptionFilter implements ExceptionFilter { const context = host.switchToHttp(); const request = context.getRequest(); const response: Response = context.getResponse(); + console.log(exception); + let targetException = exception; let responseStatusCode = 500; let responseErrorPayload: ExceptionPayload = { diff --git a/src/modules/classification/classification.service.ts b/src/modules/classification/classification.service.ts index 820c2e8..48e97e3 100644 --- a/src/modules/classification/classification.service.ts +++ b/src/modules/classification/classification.service.ts @@ -1,20 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { - Folder, - Post, - AIClassification, -} from '@src/infrastructure/database/schema'; +import { Folder, Post } from '@src/infrastructure/database/schema'; import { InjectModel } from '@nestjs/mongoose'; +import { PaginationQuery } from '@src/common'; import { Model, Types } from 'mongoose'; -import { ClassficiationRepository } from './classification.repository'; import { PostsRepository } from '../posts/posts.repository'; +import { ClassficiationRepository } from './classification.repository'; import { ClassificationFolderWithCount, PostListInClassificationFolder, } from './dto/classification.dto'; -import { PaginationQuery } from '@src/common'; -import { PostListInFolderResponse } from '../folders/responses'; @Injectable() export class ClassificationService { diff --git a/src/modules/folders/docs/folder-api.docs.ts b/src/modules/folders/docs/folder-api.docs.ts index c58fb00..aa31242 100644 --- a/src/modules/folders/docs/folder-api.docs.ts +++ b/src/modules/folders/docs/folder-api.docs.ts @@ -1,12 +1,11 @@ import { applyDecorators } from '@nestjs/common'; -import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; import { FolderListResponse, FolderResponse, FolderSummaryResponse, PostListInFolderResponse, } from '../responses'; -import { GetPostQueryDto } from '@src/modules/posts/dto/find-in-folder.dto'; export const CreateFolderDocs = applyDecorators( ApiOperation({ diff --git a/src/modules/folders/responses/post-list-in-folder.response.ts b/src/modules/folders/responses/post-list-in-folder.response.ts index d0d7e90..bdbec7a 100644 --- a/src/modules/folders/responses/post-list-in-folder.response.ts +++ b/src/modules/folders/responses/post-list-in-folder.response.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { FolderResponse } from './folder.response'; -import { PostResponse } from './post.response'; -import { PostDocument } from '@src/infrastructure'; import { PaginationMetadata } from '@src/common'; +import { Keyword, Post } from '@src/infrastructure'; +import { Types } from 'mongoose'; +import { PostResponse } from './post.response'; export class PostListInFolderResponse extends PaginationMetadata { @ApiProperty({ type: PostResponse, isArray: true }) @@ -12,7 +12,10 @@ export class PostListInFolderResponse extends PaginationMetadata { page: number, limit: number, total: number, - list: PostDocument[], + list: (Post & { + _id: Types.ObjectId; + keywords: (Keyword & { _id: Types.ObjectId })[]; + })[], ) { super(page, limit, total); diff --git a/src/modules/folders/responses/post.response.ts b/src/modules/folders/responses/post.response.ts index d5d5235..fd2e420 100644 --- a/src/modules/folders/responses/post.response.ts +++ b/src/modules/folders/responses/post.response.ts @@ -1,17 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { PostDocument } from '@src/infrastructure'; +import { Keyword, Post } from '@src/infrastructure'; +import { KeywordItem } from '@src/modules/posts/response/keyword-list.response'; +import { Types } from 'mongoose'; /** * @todo * 추후 이동 예정 */ -class Keyword { - @ApiProperty() - id: string; - - @ApiProperty() - name: string; -} /** * @todo @@ -40,14 +35,20 @@ export class PostResponse { createdAt: Date; @ApiProperty({ type: Keyword, isArray: true }) - keywords: Keyword[]; - - constructor(data: PostDocument) { + keywords: KeywordItem[]; + + constructor( + data: Post & { + _id: Types.ObjectId; + keywords: (Keyword & { _id: Types.ObjectId })[]; + }, + ) { this.id = data._id.toString(); this.folderId = data.folderId.toString(); this.url = data.url; this.title = data.title; this.description = data.description; + this.keywords = data.keywords.map((keyword) => new KeywordItem(keyword)); this.isFavorite = data.isFavorite; this.createdAt = data.createdAt; } diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts index f7680b3..90e1593 100644 --- a/src/modules/posts/posts.repository.ts +++ b/src/modules/posts/posts.repository.ts @@ -191,7 +191,8 @@ export class PostsRepository { const folders = await this.postModel .find({ folderId }) .skip(offset) - .limit(limit); + .limit(limit) + .lean(); return folders; } diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index c433011..ec84edf 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -2,12 +2,13 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { parseLinkTitleAndContent } from '@src/common'; import { IS_LOCAL } from '@src/common/constant'; -import { Keyword } from '@src/infrastructure'; +import { Keyword, Post } from '@src/infrastructure'; import { AwsLambdaService } from '@src/infrastructure/aws-lambda/aws-lambda.service'; import { AiClassificationPayload } from '@src/infrastructure/aws-lambda/type'; import { FolderType } from '@src/infrastructure/database/types/folder-type.enum'; import { CreatePostDto } from '@src/modules/posts/dto/create-post.dto'; import { PostsRepository } from '@src/modules/posts/posts.repository'; +import { FlattenMaps, Types } from 'mongoose'; import { AiClassificationService } from '../ai-classification/ai-classification.service'; import { FolderRepository } from '../folders/folders.repository'; import { ListPostQueryDto, UpdatePostDto, UpdatePostFolderDto } from './dto'; @@ -38,30 +39,11 @@ export class PostsService { ), ]); - const postIds = posts.map((post) => post._id.toString()); - const postKeywords = - await this.postKeywordsRepository.findKeywordsByPostIds(postIds); - const postKeywordMap: Record = {}; - - postKeywords.forEach((postKeyword) => { - const postId = postKeyword.postId.toString(); - if (!postKeywordMap[postId]) { - postKeywordMap[postId] = []; - } - - /** - * populate때문에 강제형변환 - */ - const keyword = postKeyword.keywordId as any as Keyword; - postKeywordMap[postId].push(keyword); - }); + const postsWithKeyword = await this.organizeFolderWithKeywords(posts); return { count, - posts: posts.map((post) => ({ - ...post, - keywords: postKeywordMap[post._id.toString()] ?? [], - })), + posts: postsWithKeyword, }; } @@ -122,7 +104,12 @@ export class PostsService { query.limit, ); - return { count, posts }; + const postsWithKeyword = await this.organizeFolderWithKeywords(posts); + + return { + count, + posts: postsWithKeyword, + }; } async updatePost(userId: string, postId: string, dto: UpdatePostDto) { @@ -173,6 +160,40 @@ export class PostsService { }); } + private async organizeFolderWithKeywords( + posts: (FlattenMaps & { _id: Types.ObjectId })[], + ) { + const postIds = posts.map((post) => post._id.toString()); + const postKeywords = + await this.postKeywordsRepository.findKeywordsByPostIds(postIds); + const postKeywordMap: Record< + string, + (Keyword & { _id: Types.ObjectId })[] + > = {}; + + postKeywords.forEach((postKeyword) => { + const postId = postKeyword.postId.toString(); + if (!postKeywordMap[postId]) { + postKeywordMap[postId] = []; + } + + /** + * populate때문에 강제형변환 + */ + const keyword = postKeyword.keywordId as any as Keyword & { + _id: Types.ObjectId; + }; + postKeywordMap[postId].push(keyword); + }); + + const postsWithKeyword = posts.map((post) => ({ + ...post, + keywords: postKeywordMap[post._id.toString()] ?? [], + })); + + return postsWithKeyword; + } + private async executeAiClassification(payload: AiClassificationPayload) { if (IS_LOCAL) { return await this.aiClassificationService.execute(payload); From 409f934233cd75d520711a1e60bdf69dfc92e37c Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:38:43 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=20=EB=82=B4=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/posts/posts.repository.ts | 10 +++++++++- src/modules/posts/posts.service.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts index 90e1593..7428985 100644 --- a/src/modules/posts/posts.repository.ts +++ b/src/modules/posts/posts.repository.ts @@ -187,10 +187,18 @@ export class PostsRepository { return count; } - async findByFolderId(folderId: string, offset: number, limit: number) { + async findByFolderId( + folderId: string, + page: number, + limit: number, + order: OrderType = OrderType.desc, + ) { + const offset = (page - 1) * limit; + const folders = await this.postModel .find({ folderId }) .skip(offset) + .sort([['createdAt', order === OrderType.desc ? -1 : 1]]) .limit(limit) .lean(); diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index ec84edf..767c107 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -96,12 +96,12 @@ export class PostsService { userId, }); - const offset = (query.page - 1) * query.limit; const count = await this.postRepository.getCountByFolderId(folderId); const posts = await this.postRepository.findByFolderId( folderId, - offset, + query.page, query.limit, + query.order, ); const postsWithKeyword = await this.organizeFolderWithKeywords(posts); From 6a6b6926bfccbcfed0c736ce2c79ab1616bb68c7 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:41:49 +0900 Subject: [PATCH 11/19] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=97=90=EB=9F=AC=20=EC=95=8C=EB=A6=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/filter/base.filter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/filter/base.filter.ts b/src/common/filter/base.filter.ts index 0aa3d86..654c25e 100644 --- a/src/common/filter/base.filter.ts +++ b/src/common/filter/base.filter.ts @@ -7,6 +7,7 @@ import { import { captureException } from '@sentry/node'; import { DiscordErrorWebhookProvider } from '@src/infrastructure/discord/discord-error-webhook.provider'; import { Response } from 'express'; +import { IS_LOCAL } from '../constant'; import { RootException, createException } from '../error'; import { ExceptionPayload, ICommonResponse } from '../types/type'; @@ -79,9 +80,12 @@ export class RootExceptionFilter implements ExceptionFilter { } private async handle(request: Request, error: Error) { - const content = this.parseError(request, error); + if (IS_LOCAL) { + return; + } - this.discordErrorWebhookProvider.send(content); + const content = this.parseError(request, error); + await this.discordErrorWebhookProvider.send(content); } private parseError(request: Request, error: Error): string { From 0f8f169dec3064d910f701603983e3c18751af54 Mon Sep 17 00:00:00 2001 From: Geonoing Date: Sun, 21 Jul 2024 03:42:24 +0900 Subject: [PATCH 12/19] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=94=94=EC=BD=94=20=EC=95=8C=EB=A6=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/filter/base.filter.ts | 5 ----- src/infrastructure/discord/discord-webhook.provider.ts | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common/filter/base.filter.ts b/src/common/filter/base.filter.ts index 654c25e..1d45c72 100644 --- a/src/common/filter/base.filter.ts +++ b/src/common/filter/base.filter.ts @@ -7,7 +7,6 @@ import { import { captureException } from '@sentry/node'; import { DiscordErrorWebhookProvider } from '@src/infrastructure/discord/discord-error-webhook.provider'; import { Response } from 'express'; -import { IS_LOCAL } from '../constant'; import { RootException, createException } from '../error'; import { ExceptionPayload, ICommonResponse } from '../types/type'; @@ -80,10 +79,6 @@ export class RootExceptionFilter implements ExceptionFilter { } private async handle(request: Request, error: Error) { - if (IS_LOCAL) { - return; - } - const content = this.parseError(request, error); await this.discordErrorWebhookProvider.send(content); } diff --git a/src/infrastructure/discord/discord-webhook.provider.ts b/src/infrastructure/discord/discord-webhook.provider.ts index 5c32036..e61a112 100644 --- a/src/infrastructure/discord/discord-webhook.provider.ts +++ b/src/infrastructure/discord/discord-webhook.provider.ts @@ -1,8 +1,14 @@ +import { IS_LOCAL } from '@src/common/constant'; + export class DiscordWebhookProvider { protected readonly webhookUrl: string; constructor() {} public async send(url: string, content: string) { + if (IS_LOCAL) { + return; + } + await fetch(url, { method: 'post', headers: { 'Content-Type': 'application/json' }, From 96fe6eda8fdad593a58f67afb41ede2e8005017a Mon Sep 17 00:00:00 2001 From: Geonoing Date: Mon, 22 Jul 2024 01:49:33 +0900 Subject: [PATCH 13/19] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8F=B0=EC=8A=A4=EC=97=90=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EC=9E=91=EC=97=85=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EB=B3=B8=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/posts/posts.repository.ts | 4 ++-- src/modules/posts/posts.service.ts | 9 ++++++--- src/modules/posts/response/listPost.response.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts index db454da..bfc3f85 100644 --- a/src/modules/posts/posts.repository.ts +++ b/src/modules/posts/posts.repository.ts @@ -100,7 +100,7 @@ export class PostsRepository { title: string, thumbnail: string, postAIStatus: PostAiStatus, - ): Promise { + ) { const postModel = await this.postModel.create({ folderId: folderId, url: url, @@ -110,7 +110,7 @@ export class PostsRepository { thumbnailImgUrl: thumbnail, aiStatus: postAIStatus, }); - return postModel; + return postModel.toObject(); } async getPostCountByFolderIds(folderIds: Types.ObjectId[]) { diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index f5d1a60..f57be90 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -20,6 +20,7 @@ import { } from './dto'; import { GetPostQueryDto } from './dto/find-in-folder.dto'; import { PostKeywordsRepository } from './postKeywords.repository'; +import { PostItemDto } from './response'; @Injectable() export class PostsService { @@ -70,7 +71,7 @@ export class PostsService { async createPost( createPostDto: CreatePostDto, userId: string, - ): Promise { + ): Promise { // NOTE : URL에서 얻은 정보 가져옴 const { title, content, thumbnail } = await parseLinkTitleAndContent( createPostDto.url, @@ -101,7 +102,7 @@ export class PostsService { await this.executeAiClassification(payload); - return post; + return { ...post, keywords: [] } satisfies PostItemDto; } /** @@ -152,7 +153,9 @@ export class PostsService { const post = await this.postRepository.findPostOrThrow({ _id: postId, }); - return post; + + const [postsWithKeyword] = await this.organizeFolderWithKeywords([post]); + return postsWithKeyword; } async updatePostFolder( diff --git a/src/modules/posts/response/listPost.response.ts b/src/modules/posts/response/listPost.response.ts index 56018ab..3509968 100644 --- a/src/modules/posts/response/listPost.response.ts +++ b/src/modules/posts/response/listPost.response.ts @@ -5,6 +5,11 @@ import { PostAiStatus } from '@src/modules/posts/posts.constant'; import { Types } from 'mongoose'; import { KeywordItem } from './keyword-list.response'; +export type PostItemDto = Post & { + _id: Types.ObjectId; + keywords: (Keyword & { _id: Types.ObjectId })[]; +}; + export class ListPostItem { @ApiProperty({ required: true, description: '피드 id', type: String }) id: string; @@ -47,12 +52,7 @@ export class ListPostItem { }) aiStatus: PostAiStatus; - constructor( - data: Post & { - _id: Types.ObjectId; - keywords: (Keyword & { _id: Types.ObjectId })[]; - }, - ) { + constructor(data: PostItemDto) { this.id = data._id.toString(); this.folderId = data.folderId.toString(); this.url = data.url; From b270fcdc17db0c07966eb5d9bcfc1e5fa94163ad Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:20:32 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20=ED=8F=B4=EB=8D=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20post=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/folders/folders.controller.ts | 1 + src/modules/posts/posts.service.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/modules/folders/folders.controller.ts b/src/modules/folders/folders.controller.ts index 9973cdf..42c1d07 100644 --- a/src/modules/folders/folders.controller.ts +++ b/src/modules/folders/folders.controller.ts @@ -127,5 +127,6 @@ export class FoldersController { @Delete(':folderId') async remove(@GetUser() userId: string, @Param('folderId') folderId: string) { await this.foldersService.remove(userId, folderId); + await this.postsService.removePostListByFolderId(userId, folderId); } } diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index f57be90..aa6588a 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -195,6 +195,13 @@ export class PostsService { return true; } + async removePostListByFolderId(userId: string, folderId: string) { + await this.postRepository.deleteMany({ + userId, + folderId, + }); + } + async removeAllPostsInCustomFolders(userId: string) { const customFolders = await this.folderRepository.findByUserId(userId); const customFolderIds = customFolders From 6d38e332691379a08ab1f7d756a1d37e98f5a759 Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:39:56 +0900 Subject: [PATCH 15/19] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=20=EC=A0=84=EC=B2=B4=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=8B=9C=20folderIdList=20=EB=B0=98=ED=99=98=EC=9C=BC?= =?UTF-8?q?=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 --- src/modules/posts/posts.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index aa6588a..8e30cf4 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -202,7 +202,7 @@ export class PostsService { }); } - async removeAllPostsInCustomFolders(userId: string) { + async removeAllPostsInCustomFolders(userId: string): Promise { const customFolders = await this.folderRepository.findByUserId(userId); const customFolderIds = customFolders .filter((folder) => folder.type === FolderType.CUSTOM) @@ -212,6 +212,7 @@ export class PostsService { userId, folderId: { $in: customFolderIds }, }); + return customFolderIds.map((folder) => folder.toString()); } private async organizeFolderWithKeywords( From eda33ec07cdf7dc3e6bf3e8c31c20d069af54ef5 Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:03:18 +0900 Subject: [PATCH 16/19] refactor: classification service export --- src/modules/classification/classification.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/classification/classification.module.ts b/src/modules/classification/classification.module.ts index c6b64b5..5a60e58 100644 --- a/src/modules/classification/classification.module.ts +++ b/src/modules/classification/classification.module.ts @@ -26,5 +26,6 @@ import { ClassficiationRepository } from './classification.repository'; ], controllers: [ClassificationController], providers: [ClassificationService, ClassficiationRepository, PostsRepository], + exports: [ClassificationService], }) export class ClassificationModule {} From b3a2a3b5b3c27bf1bed35e39c3822f285b26e964 Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:03:36 +0900 Subject: [PATCH 17/19] refactor: classification module forwardRef --- src/modules/folders/folders.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/folders/folders.module.ts b/src/modules/folders/folders.module.ts index 2d0dc92..913a2e7 100644 --- a/src/modules/folders/folders.module.ts +++ b/src/modules/folders/folders.module.ts @@ -1,13 +1,15 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Folder, FolderSchema, Post, PostSchema } from '@src/infrastructure'; import { PostsModule } from '../posts/posts.module'; import { FoldersController } from './folders.controller'; import { FolderRepository } from './folders.repository'; import { FoldersService } from './folders.service'; +import { ClassificationModule } from '@src/modules/classification/classification.module'; @Module({ imports: [ + forwardRef(() => ClassificationModule), MongooseModule.forFeature([ { name: Post.name, schema: PostSchema }, { name: Folder.name, schema: FolderSchema }, From 12d066c208a0b96cdd8a45985bd0917cd068f57b Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:04:39 +0900 Subject: [PATCH 18/19] feat: add delete classificationList at suggestedFolder delete --- src/modules/folders/folders.controller.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/folders/folders.controller.ts b/src/modules/folders/folders.controller.ts index 42c1d07..67ed5d9 100644 --- a/src/modules/folders/folders.controller.ts +++ b/src/modules/folders/folders.controller.ts @@ -31,6 +31,7 @@ import { FolderSummaryResponse, } from './responses'; import { PostResponse } from './responses/post.response'; +import { ClassificationService } from '@src/modules/classification/classification.service'; @FolderControllerDocs @UseGuards(JwtGuard) @@ -39,6 +40,7 @@ export class FoldersController { constructor( private readonly foldersService: FoldersService, private readonly postsService: PostsService, + private readonly classificationService: ClassificationService, ) {} @CreateFolderDocs @@ -115,17 +117,23 @@ export class FoldersController { @Delete('/all') async removeAll(@Query() deleteCustomFolderDto: DeleteCustomFolderDto) { - await this.postsService.removeAllPostsInCustomFolders( + const folderIdList = await this.postsService.removeAllPostsInCustomFolders( deleteCustomFolderDto.userId, ); await this.foldersService.removeAllCustomFolders( deleteCustomFolderDto.userId, ); + await this.classificationService.deleteClassificationBySuggestedFolderId( + folderIdList, + ); } @DeleteFolderDocs @Delete(':folderId') async remove(@GetUser() userId: string, @Param('folderId') folderId: string) { + await this.classificationService.deleteClassificationBySuggestedFolderId( + folderId, + ); await this.foldersService.remove(userId, folderId); await this.postsService.removePostListByFolderId(userId, folderId); } From 130b4af6c348da4baef04b2f9f96681f67900cf6 Mon Sep 17 00:00:00 2001 From: Anjonghun <58875626+JonghunAn@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:05:16 +0900 Subject: [PATCH 19/19] feat: soft-delete classification rows --- .../classification/classification.repository.ts | 14 ++++++++++++++ .../classification/classification.service.ts | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/modules/classification/classification.repository.ts b/src/modules/classification/classification.repository.ts index b42b909..4af2abb 100644 --- a/src/modules/classification/classification.repository.ts +++ b/src/modules/classification/classification.repository.ts @@ -167,4 +167,18 @@ export class ClassficiationRepository { ) .exec(); } + + async deleteManyBySuggestedFolderIdList( + suggestedFolderId: string[] | string, + ): Promise { + await this.aiClassificationModel + .updateMany( + { suggestedFolderId: { $in: suggestedFolderId } }, + { + deletedAt: new Date(), + }, + ) + .exec(); + return true; + } } diff --git a/src/modules/classification/classification.service.ts b/src/modules/classification/classification.service.ts index b695ff7..6227358 100644 --- a/src/modules/classification/classification.service.ts +++ b/src/modules/classification/classification.service.ts @@ -137,4 +137,12 @@ export class ClassificationService { post.aiClassificationId.toString(), ); } + + async deleteClassificationBySuggestedFolderId( + suggestedFolderId: string[] | string, + ): Promise { + return await this.classficationRepository.deleteManyBySuggestedFolderIdList( + suggestedFolderId, + ); + } }