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 : classification post list get api, 폴더별 post list get api 수정, 폴더 이름 list get api 에 정렬 적용 #39

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9fb09d1
Merge branch 'feat/foler-api' into dev-deploy
Marades Jun 29, 2024
89c27a1
Merge branch 'development' into dev-deploy
Marades Jun 29, 2024
cbb9130
Merge branch 'feat/foler-api' into dev-deploy
Marades Jun 29, 2024
6329a8c
Merge branch 'feat/foler-api' into dev-deploy
Marades Jun 29, 2024
25a8340
Feat : ai가 추천해주는 폴더로 post 모두 이동 api 구현
hye-on Jul 2, 2024
9641e26
Chore : 스키마 이름 수정
hye-on Jul 2, 2024
301c4b4
Feat : ai 분류 추천 리스트에서 링크 삭제 delete api 구현
hye-on Jul 3, 2024
db3cbe2
refactor : repository 생성
hye-on Jul 3, 2024
316bbb3
Chore : dto import 누락 수정
hye-on Jul 3, 2024
2634a65
refactor: method naming to camel-case
JonghunAn Jul 3, 2024
f818d60
Merge branch 'dev-deploy' into feature/ai-lambda
JonghunAn Jul 3, 2024
48fb6e5
Merge pull request #36 from mash-up-kr/feature/ai-lambda
JonghunAn Jul 3, 2024
7957b23
Feat : 폴더 이름 list에 정렬 추가
hye-on Jul 4, 2024
d89506b
Feat : 추천폴더별 링크 리스트에 페이지네이션 적용
hye-on Jul 5, 2024
ef224a2
Chore : controller.docs.ts에 ApiBearerAuth() 추가
hye-on Jul 5, 2024
2e6ef65
Chore : classification 폴더 이름 리스트 response에 totalCounts(분류한 링크 개수) 추가
hye-on Jul 5, 2024
f032141
Chore : 폴더별 링크 리스트 get api 이름 수정
hye-on Jul 5, 2024
8cfdc18
Chore : 분류 페이지 내 폴더 이름 get api 의 path 수정
hye-on Jul 5, 2024
03c0d81
Chore : 링크 리스트 get api path 수정
hye-on Jul 5, 2024
f22fdb6
Feat : ai 분류 페이지 내 전체 post list get api 추가, 폴더 별 post list api 수정
hye-on Jul 5, 2024
d098a3f
Merge remote-tracking branch 'origin/dev-deploy' into feature/classif…
hye-on Jul 5, 2024
797db2b
env : 윈도우 이슈로 수정..
hye-on Jul 5, 2024
024a732
Feat: Add count classified count API
J-Hoplin Jul 5, 2024
5500975
Merge branch 'feature/classification-list-api' of https://github.com/…
J-Hoplin Jul 5, 2024
d9d635e
Chore: Pnpm conflict
J-Hoplin Jul 5, 2024
f605f08
Fix: Modify method name of countClassifiedPost(controller, service, r…
J-Hoplin Jul 7, 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
4 changes: 2 additions & 2 deletions src/infrastructure/database/schema/AIClassification.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ export class AIClassification extends BaseDocument {
deletedAt: Date;
}

export type PostAIClassificationDocument = HydratedDocument<AIClassification>;
export const PostAIClassificationSchema =
export type AIClassificationDocument = HydratedDocument<AIClassification>;
export const AIClassificationSchema =
SchemaFactory.createForClass(AIClassification);
3 changes: 2 additions & 1 deletion src/infrastructure/database/schema/post.schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { AIClassification } from './AIClassification.schema';
import { BaseDocument } from './base.schema';

@Schema({ collection: 'posts', timestamps: true, versionKey: false })
export class Post {
export class Post extends BaseDocument {
@Prop({ required: true, type: MongooseSchema.Types.ObjectId, ref: 'User' })
userId!: MongooseSchema.Types.ObjectId;

Expand Down
63 changes: 53 additions & 10 deletions src/modules/classification/classification.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Controller, Get, UseGuards, Param } from '@nestjs/common';
import {
Controller,
Get,
UseGuards,
Param,
Query,
Delete,
Patch,
} from '@nestjs/common';
import { ClassificationService } from './classification.service';
import { GetUser } from '@src/common';
import { GetUser, PaginationQuery } from '@src/common';
import {
ClassificationControllerDocs,
GetAIFolderNameListDocs,
GetAIPostListDocs,
DeleteAIClassificationDocs,
PatchAIPostDocs,
} from './docs';
import { JwtGuard } from '../users/guards';
import { AIFolderNameListResponse } from './response/ai-folder-list.dto';
Expand All @@ -16,26 +26,59 @@ import { AIPostListResponse } from './response/ai-post-list.dto';
export class ClassificationController {
constructor(private readonly classificationService: ClassificationService) {}

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

return new AIFolderNameListResponse(folderNames);
return new AIFolderNameListResponse(folders);
}

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

return new AIPostListResponse(posts);
}
@Get('/posts/:folderId')
@GetAIPostListDocs
async getSuggestedPostListInFolder(
@GetUser() userId: string,
@Param('folderId') folderId: string,
@Query() paingQuery: PaginationQuery,
) {
const posts = await this.classificationService.getPostListInFolder(
userId,
folderId,
paingQuery,
);

return new AIPostListResponse(posts);
}
@Patch('/posts')
@PatchAIPostDocs
async moveAllPost(
@GetUser() userId: string,
@Query('suggestionFolderId') suggestionFolderId: string,
) {
await this.classificationService.moveAllPostTosuggestionFolder(
userId,
suggestionFolderId,
);
}

@Delete('/posts/:postId')
@DeleteAIClassificationDocs
async abortClassification(
@GetUser() userId: string,
@Query('postId') postId: string,
) {
await this.classificationService.abortClassification(userId, postId);
}
Comment on lines +70 to +89
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이전 pr에 있는 내용..

}
8 changes: 5 additions & 3 deletions src/modules/classification/classification.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@ import {
FolderSchema,
Post,
AIClassification,
PostAIClassificationSchema,
AIClassificationSchema,
PostSchema,
} from '@src/infrastructure/database/schema';

import { ClassificationController } from './classification.controller';
import { ClassficiationRepository } from './classification.repository';
import { PostsRepository } from '../posts/posts.repository';

@Module({
imports: [
MongooseModule.forFeature([
{ name: Folder.name, schema: FolderSchema },
{ name: AIClassification.name, schema: PostAIClassificationSchema },
{ name: AIClassification.name, schema: AIClassificationSchema },
{ name: Post.name, schema: PostSchema },
]),
],
controllers: [ClassificationController],
providers: [ClassificationService],
providers: [ClassificationService, ClassficiationRepository, PostsRepository],
})
export class ClassificationModule {}
80 changes: 80 additions & 0 deletions src/modules/classification/classification.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types, Schema } from 'mongoose';
import { AIClassification, Folder } from '@src/infrastructure';
import { ClassificationFolderWithCount } from './dto/classification.dto';

@Injectable()
export class ClassficiationRepository {
constructor(
@InjectModel(AIClassification.name)
private readonly aiClassificationModel: Model<AIClassification>,
) {}

async findContainedFolderByUserId(
userId: Types.ObjectId,
): Promise<ClassificationFolderWithCount[]> {
return await this.aiClassificationModel
.aggregate([
{
$match: {
deletedAt: null,
},
},
{
$lookup: {
from: 'folders',
localField: 'suggestedFolderId',
foreignField: '_id',
as: 'folder',
},
},
{
$unwind: '$folder',
},
{
$match: {
'folder.userId': userId,
},
},
{
$group: {
_id: '$suggestedFolderId',
folderName: { $first: '$folder.name' },
postCount: { $sum: 1 },
folderCreatedAt: { $first: '$folder.createdAt' },
},
},
{
$sort: {
postCount: -1,
folderCreatedAt: -1,
},
},
{
$project: {
_id: 0,
folderId: { $toString: '$_id' },
folderName: 1,
postCount: 1,
},
},
])
.exec();
}

async delete(id: string) {
await this.aiClassificationModel
.findByIdAndUpdate(id, { deletedAt: new Date() })
.exec();
}

async deleteBySuggestedFolderId(suggestedFolderId: string) {
await this.aiClassificationModel
.updateMany(
{ suggestedFolderId: suggestedFolderId, deletedAt: null },
{ $set: { deletedAt: new Date() } },
)
.exec();
}
}
108 changes: 76 additions & 32 deletions src/modules/classification/classification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,96 @@ import {

import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { AIFolderNameServiceDto } from './dto/getAIFolderNameLIst.dto';
import { AIPostServiceDto } from './dto/getAIPostList.dto';
import { ClassficiationRepository } from './classification.repository';
import { PostsRepository } from '../posts/posts.repository';
import {
ClassificationFolderWithCount,
PostListInClassificationFolder,
} from './dto/classification.dto';
import { PaginationQuery } from '@src/common';
import { PostListInFolderResponse } from '../folders/responses';

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

async getFolderNameList(userId: String): Promise<AIFolderNameServiceDto[]> {
const folders = await this.folderModel.find({ userId }).exec();
const folderIds = folders.map((folder) => folder._id);
async getFolderNameList(
userId: string,
): Promise<ClassificationFolderWithCount[]> {
return await this.classficationRepository.findContainedFolderByUserId(
new Types.ObjectId(userId),
);
}

const classificationIds = await this.postAiClassificationModel
.distinct('suggestedFolderId')
.where('suggestedFolderId')
.in(folderIds)
.exec();
async getPostList(userId: string, paingQuery: PaginationQuery) {
const orderedFolderIdList = await this.getFolderOrder(userId);

const matchedFolders = await this.folderModel
.find({ _id: { $in: classificationIds } })
.exec();
const offset = (paingQuery.page - 1) * paingQuery.limit;
return await this.postRepository.findAndSortBySuggestedFolderIds(
new Types.ObjectId(userId),
orderedFolderIdList,
offset,
paingQuery.limit,
);
}

return matchedFolders.map((folder) => new AIFolderNameServiceDto(folder));
async getFolderOrder(userId: string) {
const orderedFolderList =
await this.classficationRepository.findContainedFolderByUserId(
new Types.ObjectId(userId),
);

return orderedFolderList.map(
(folder) => new Types.ObjectId(folder.folderId),
);
}

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

return posts
.filter((post) => post.aiClassificationId)
.map((post) => new AIPostServiceDto(post, post.aiClassificationId));
paingQuery: PaginationQuery,
): Promise<PostListInClassificationFolder[]> {
const offset = (paingQuery.page - 1) * paingQuery.limit;

return await this.postRepository.findBySuggestedFolderId(
userId,
new Types.ObjectId(folderId),
offset,
paingQuery.limit,
);
}
async moveAllPostTosuggestionFolder(
userId: string,
suggestedFolderId: string,
) {
const postIdList =
await this.postRepository.findFolderIdsBySuggestedFolderId(
userId,
suggestedFolderId,
);

for (const post of postIdList) {
await this.postRepository.updateFolderId(
post._id.toString(),
suggestedFolderId,
);
}

await this.classficationRepository.deleteBySuggestedFolderId(
suggestedFolderId,
);
}

async abortClassification(userId: string, postId: string) {
const post = await this.postModel.findById(postId).exec();

await this.classficationRepository.delete(
post.aiClassificationId.toString(),
);
}
}
3 changes: 2 additions & 1 deletion src/modules/classification/docs/controller.docs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { applyDecorators } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';

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

export const DeleteAIClassificationDocs = applyDecorators(
ApiOperation({
summary: 'ai 분류 추천 리스트에서 삭제',
description:
'ai 분류 추천 리스트에서 삭제합니다. 나중에 읽을 폴더에 계속 위치됩니다.',
}),
ApiResponse({}),
ApiBearerAuth(),
);
2 changes: 1 addition & 1 deletion src/modules/classification/docs/getAIPostList.docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AIPostListResponse } from '../response/ai-post-list.dto';

export const GetAIPostListDocs = applyDecorators(
ApiOperation({
summary: '폴더 안에 들어있는 Post(링크) 리스트',
summary: 'Post(링크) 리스트',
description: 'AI 분류 추천된 링크 리스트.',
}),
ApiResponse({
Expand Down
2 changes: 2 additions & 0 deletions src/modules/classification/docs/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './controller.docs';
export * from './getAIFolderNameList.docs';
export * from './getAIPostList.docs';
export * from './patchAIPost.docs';
export * from './deleteAIClassification.docs';
Loading