Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat : AI 분류 폴더 이름 리스트get api, AI 분류 링크 리스트 get api #26

Merged
merged 22 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8c60056
Feature : AI로 분류 추천한 폴더 이름 리스트 get api
hye-on Jun 29, 2024
1ec8b47
Merge branch 'development' into feature/ai-domain
hye-on Jun 29, 2024
2c99b7e
Chore : BaseDocument 날짜 필드 string-> Date로 타입수정
hye-on Jun 29, 2024
a5b1ac2
Chore : PostAIClassification의 keywords타입을 string으로 변경
hye-on Jun 29, 2024
8a9d898
Featrue : AI분류 폴더 이름 List Get api, AI 분류 각 폴더의 링크 List Get api 구현
hye-on Jun 29, 2024
77580bd
Merge branch 'development' into feature/ai-domain
hye-on Jun 29, 2024
e9abb46
Chore : 주석 제거
hye-on Jun 29, 2024
63e5f58
Chore : deletedAt이 null인 것만 가져오게 수정
hye-on Jun 29, 2024
ccf6809
Chore : 링크 리스트 get api response에 id 추가
hye-on Jun 29, 2024
68ade94
Chore : 리뷰 반영. getAIFolderNameList dto 포맷팅 수정
hye-on Jul 2, 2024
aea6e40
Chore : TODO 주석 삭제
hye-on Jul 2, 2024
53e166c
Chore : distinct를 이용해 중복제거하게 수정
hye-on Jul 2, 2024
09892b7
Chore : 리뷰 반영. mergeType을 스키마별로 분리해서 받게 수정.
hye-on Jul 2, 2024
9764ef8
Chore : 리뷰 반영. 링크리스트가져올 때 userId도 체크하게 수정
hye-on Jul 2, 2024
6f4f0f3
Chore : 필요없는 타입 삭제
hye-on Jul 2, 2024
e4e0f1c
Chore : 리뷰 반영. 서비스 dto에서 필요없는 데코레이터 삭제
hye-on Jul 2, 2024
33f950a
Chore : 리뷰 반영. 컨트롤러 path 변경. ai->classification
hye-on Jul 2, 2024
0df80c4
Chore : 리뷰 반영. 스키마 이름 수정. PostAIClassification->AIClassification
hye-on Jul 2, 2024
bd3c19b
Chore : 리뷰 반영. 서비스 dto에 스웨거 관련 데코레이터 삭제
hye-on Jul 2, 2024
68e4b20
Chore : 리뷰 반영. response dto 들 response아래로 위치 이동
hye-on Jul 2, 2024
b3c8851
Chore : 리뷰 반영. 스키마 이름 수정
hye-on Jul 2, 2024
e1f50ef
Merge branch 'development' into feature/ai-domain
J-Hoplin Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12,536 changes: 5,619 additions & 6,917 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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';
Expand All @@ -23,6 +24,7 @@ import { PostsModule } from './modules/posts/posts.module';
DatabaseModule,
AiModule,
UsersModule,
ClassificationModule,
AuthModule,
FoldersModule,
LinksModule,
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
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;
}
1 change: 1 addition & 0 deletions src/infrastructure/database/schema/post.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class Post {

@Prop({ required: true })
title!: string;
//TODO : 사진 추가
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(˵ •̀ ᴗ - ˵ ) ✧
사진은 프론트에서 url 전달주면 opengraph로 가져오기로함! 필드 없어도 돼

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


@Prop({ default: null, type: String })
description: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {
Keyword,
KeywordSchema,
} from '@src/infrastructure/database/schema/keyword.schema';
import { BaseDocument } from './base.schema';

@Schema({
collection: 'post_ai_classifications',
timestamps: true,
versionKey: false,
})
export class PostAIClassification {
export class PostAIClassification extends BaseDocument {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export class PostAIClassification extends BaseDocument {
export class AIClassification extends BaseDocument {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Prop({ required: true, type: MongooseSchema.Types.ObjectId, ref: 'Folder' })
suggestedFolderId: MongooseSchema.Types.ObjectId;

Expand All @@ -20,13 +21,13 @@ 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;
}

Expand Down
37 changes: 37 additions & 0 deletions src/modules/classification/classification.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 { Types } from 'mongoose';
import { AIPostListResponse } from './dto/getAIPostList.dto';
import { AIFolderNameListResponse } from './dto/getAIFolderNameLIst.dto';

@Controller('ai')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Controller('ai')
@Controller('classification')

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

return new AIFolderNameListResponse(folderNames);
}

@Get('/suggestions/:folderId')
@GetAIPostListDocs
async getSuggestedPostList(@Param('folderId') folderId: string) {
const posts = await this.classificationService.getPostList(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,
PostAIClassification,
PostAIClassificationSchema,
PostSchema,
} from '@src/infrastructure/database/schema';

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

@Module({
imports: [
MongooseModule.forFeature([
{ name: Folder.name, schema: FolderSchema },
{ name: PostAIClassification.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();
});
});
64 changes: 64 additions & 0 deletions src/modules/classification/classification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
import {
Folder,
FolderDocument,
Post,
PostAIClassification,
} 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(PostAIClassification.name)
private postAiClassificationModel: Model<PostAIClassification>,
@InjectModel(Post.name) private postModel: Model<Post>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 나중에 repo로 빼보면 좋을 거 같당

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키!!

) {}

async getFolderNameList(
userId: Types.ObjectId,
): Promise<AIFolderNameServiceDto[]> {
const folders = await this.folderModel.find({ userId }).exec();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 내가 folderRepo에 findByUserId라는 메소드로 만들어놨으~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오빠꺼 아직 머지 안되서 머지되면 수정할께!

const folderIds = folders.map((folder) => folder._id);
Comment on lines +23 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

**(/¯◡‿◡)/¯✧·˚ : *✧·˚ : ***
이거 폴더 id만 필요한거면 folders 가져와서 map 하지말고 요렇게 하는 방법도 있네~
근데 folder schema는 필드가 많지않아서 건회가 만들어둔 findByUserId 써두 될듯~

https://www.zerocho.com/category/MongoDB/post/59bd148b1474c800194b695a
image

Suggested change
const folders = await this.folderModel.find({ userId }).exec();
const folderIds = folders.map((folder) => folder._id);
const folders = await this.folderModel.find({ userId }).select('')exec();
const folderIds = folders.map((folder) => folder._id);

Copy link
Collaborator Author

@hye-on hye-on Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 땡큐
건회오빠꺼 머지되면 만들어둔 findByUserId 써서 수정할께!


const classifications = await this.postAiClassificationModel
.find({ suggestedFolderId: { $in: folderIds } })
.exec();

const uniqueFolderIds = [
...new Set(
classifications.map((classification) =>
classification.suggestedFolderId.toString(),
),
),
];
Copy link
Member

@JonghunAn JonghunAn Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

˵ •̀ ᴗ - ˵ ) ✧

Suggested change
const classifications = await this.postAiClassificationModel
.find({ suggestedFolderId: { $in: folderIds } })
.exec();
const uniqueFolderIds = [
...new Set(
classifications.map((classification) =>
classification.suggestedFolderId.toString(),
),
),
];
const classifications = await this.postAiClassificationModel.distinct(folder_id).
.find({ suggestedFolderId: { $in: folderIds } })
.exec();

폴더 아이디를 중복을 제외하고 가져오고 싶으면 Set 하지말고 쿼리에서 distinct 쓰는게 맞을 것 같아~
문법은 지원하는데 저렇게 쓰는지는 한번 테스트해봐~
https://mongoosejs.com/docs/api/query.html#Query.prototype.distinct()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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

return matchedFolders.map((folder) => new AIFolderNameServiceDto(folder));
J-Hoplin marked this conversation as resolved.
Show resolved Hide resolved
}

async getPostList(folderId: string): Promise<AIPostServiceDto[]> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혜온아 이부분도 @GetUser로 사용자 ID 가져와서 사용자 분기도 같이 해줘야할것 같아!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const posts = await this.postModel
.find({ folderId })
.populate<{
aiClassificationId: PostAIClassification;
}>({
path: 'aiClassificationId',
match: { deletedAt: null }, // deletedAt이 null인 것만 필터링
})
.sort({ createdAt: -1 })
.exec();

return posts
.filter((post) => post.aiClassificationId)
.map((post) => new AIPostServiceDto(post));
}
}
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 '../dto/getAIFolderNameLIst.dto';

export const GetAIFolderNameListDocs = applyDecorators(
ApiOperation({
summary: '폴더 리스트',
description: 'AI 분류 폴더 리스트.',
}),
ApiResponse({
type: AIFolderNameListResponse,
}),
ApiBearerAuth(),
);
14 changes: 14 additions & 0 deletions src/modules/classification/docs/getAIPostList.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 { AIPostListResponse } from '../dto/getAIPostList.dto';

export const GetAIPostListDocs = applyDecorators(
ApiOperation({
summary: '폴더 안에 들어있는 Post(링크) 리스트',
description: 'AI 분류 추천된 링크 리스트.',
}),
ApiResponse({
type: AIPostListResponse,
}),
ApiBearerAuth(),
);
3 changes: 3 additions & 0 deletions src/modules/classification/docs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './controller.docs';
export * from './getAIFolderNameList.docs';
export * from './getAIPostList.docs';
31 changes: 31 additions & 0 deletions src/modules/classification/dto/getAIFolderNameLIst.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { FolderDocument } from '@src/infrastructure';
import { IsNotEmpty, IsString } from 'class-validator';

export class AIFolderNameServiceDto {
@ApiProperty({ description: '폴더 id' })
@IsNotEmpty()
@IsString()
Copy link
Collaborator

@Marades Marades Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(˵ •̀ ᴗ - ˵ ) ✧
service dto라서 요 데코레이터들도 빼도 될 거 같아!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서비스 dto에서 스웨거 관련 데코레이터도 제거해?
스웨거에서 스키마 볼 때 필요하지 않나해서

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id: string;

@ApiProperty({ description: '폴더 이름' })
@IsNotEmpty()
@IsString()
name: string;

constructor(data: FolderDocument) {
(this.id = data._id.toString()), (this.name = data.name);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분 먼가 포매팅이 잘못 된 거 같당

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

export class AIFolderNameListResponse {
@ApiProperty({
type: AIFolderNameServiceDto,
isArray: true,
})
list: AIFolderNameServiceDto[];

constructor(data: AIFolderNameServiceDto[]) {
this.list = data;
}
}
71 changes: 71 additions & 0 deletions src/modules/classification/dto/getAIPostList.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsArray, IsDate } from 'class-validator';
import { Type } from 'class-transformer';
import { Post, PostAIClassification, PostDocument } from '@src/infrastructure';
import { Document } from 'mongoose';
import { MergeType } from 'mongoose';
import { Types } from 'mongoose';

type InputType = Document<
unknown,
{},
MergeType<
Post,
{
aiClassificationId: PostAIClassification;
}
>
> &
Omit<Post, 'aiClassificationId'> & {
aiClassificationId: PostAIClassification;
} & {
_id: Types.ObjectId;
};
Copy link
Member

@JonghunAn JonghunAn Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(˵ •̀ ᴗ - ˵ ) ✧
요건 serviceDto에서 constructor에서 paramter 받을때 도메인 스키마별로 받으면 필요없음!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export class AIPostServiceDto {
@ApiProperty({ description: 'Id' })
@IsNotEmpty()
@IsString()
id: string;

@ApiProperty({ description: 'Title' })
@IsNotEmpty()
@IsString()
title: string;

@ApiProperty({ description: 'URL' })
@IsNotEmpty()
@IsString()
url: string;

@ApiProperty({ description: 'Description' })
@IsNotEmpty()
@IsString()
description: string;

@ApiProperty({ description: 'Keywords' })
@IsArray()
keywords: string[];

@ApiProperty({ description: 'Created At' })
@IsString()
createdAt: Date;

constructor(data: InputType) {
this.id = data.id;
this.title = data.title;
this.url = data.url;
this.description = data.description;
this.keywords = data.aiClassificationId.keywords;
this.createdAt = data.aiClassificationId.createdAt;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
constructor(data: InputType) {
this.id = data.id;
this.title = data.title;
this.url = data.url;
this.description = data.description;
this.keywords = data.aiClassificationId.keywords;
this.createdAt = data.aiClassificationId.createdAt;
}
constructor(post: postSchema, ai: aiClassificationSchema ) {
this.id = post.id;
this.title = post.title;
this.url = post.url;
this.description = post.description;
this.keywords = ai.keywords;
this.createdAt = ai.aiClassificationId.createdAt;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

export class AIPostListResponse {
@ApiProperty({ type: [AIPostServiceDto] })
@Type(() => AIPostServiceDto)
list: AIPostServiceDto[];

constructor(data: AIPostServiceDto[]) {
this.list = data;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(˵ •̀ ᴗ - ˵ ) ✧
ResponseDto는 /classification/response 아래에 별도 클래스 파일로 분리해줘~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 changes: 1 addition & 1 deletion src/modules/folders/responses/folder-summary.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class FolderSummaryResponse {
type: FolderType;

@ApiProperty()
createdAt: string;
createdAt: Date;

constructor(data: FolderDocument) {
this.id = data._id.toString();
Expand Down
Loading