diff --git a/backend/package.json b/backend/package.json index 5fc7e834..f4f00038 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,6 +39,8 @@ "dotenv": "^16.4.5", "ioredis": "^5.4.1", "mysql2": "^3.11.4", + "nestjs-paginate": "^10.0.0", + "nestjs-redis-om": "^0.1.2", "passport": "^0.7.0", "passport-custom": "^1.1.1", "passport-jwt": "^4.0.1", diff --git a/backend/src/question-list/dto/my-question-list.dto.ts b/backend/src/question-list/dto/my-question-list.dto.ts index bc8ff9eb..c568456d 100644 --- a/backend/src/question-list/dto/my-question-list.dto.ts +++ b/backend/src/question-list/dto/my-question-list.dto.ts @@ -1,9 +1,6 @@ -import { Question } from "../question.entity"; - export interface MyQuestionListDto { id: number; title: string; - contents: Question[]; categoryNames: string[]; isPublic: boolean; usage: number; diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 9f767306..22552f25 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -1,4 +1,15 @@ -import { Body, Controller, Delete, Get, Param, Post, Req, Res, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + Req, + Res, + UseGuards, +} from "@nestjs/common"; import { QuestionListService } from "./question-list.service"; import { CreateQuestionListDto } from "./dto/create-question-list.dto"; import { GetAllQuestionListDto } from "./dto/get-all-question-list.dto"; @@ -7,21 +18,23 @@ import { AuthGuard } from "@nestjs/passport"; import { JwtPayload } from "@/auth/jwt/jwt.decorator"; import { IJwtPayload } from "@/auth/jwt/jwt.model"; import { MyQuestionListDto } from "./dto/my-question-list.dto"; +import { PaginateQuery } from "nestjs-paginate"; @Controller("question-list") export class QuestionListController { constructor(private readonly questionListService: QuestionListService) {} @Get() - async getAllQuestionLists(@Res() res) { + async getAllQuestionLists(@Query() query: PaginateQuery, @Res() res) { try { - const allQuestionLists: GetAllQuestionListDto[] = - await this.questionListService.getAllQuestionLists(); + const { allQuestionLists, meta } = + await this.questionListService.getAllQuestionLists(query); return res.send({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, + meta, }, }); } catch (error) { @@ -82,6 +95,7 @@ export class QuestionListController { @Post("category") async getAllQuestionListsByCategoryName( + @Query() query: PaginateQuery, @Res() res, @Body() body: { @@ -90,13 +104,17 @@ export class QuestionListController { ) { try { const { categoryName } = body; - const allQuestionLists: GetAllQuestionListDto[] = - await this.questionListService.getAllQuestionListsByCategoryName(categoryName); + const { allQuestionLists, meta } = + await this.questionListService.getAllQuestionListsByCategoryName( + categoryName, + query + ); return res.send({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, + meta, }, }); } catch (error) { @@ -138,16 +156,23 @@ export class QuestionListController { @Get("my") @UseGuards(AuthGuard("jwt")) - async getMyQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { + async getMyQuestionLists( + @Query() query: PaginateQuery, + @Res() res, + @JwtPayload() token: IJwtPayload + ) { try { const userId = token.userId; - const myQuestionLists: MyQuestionListDto[] = - await this.questionListService.getMyQuestionLists(userId); + const { myQuestionLists, meta } = await this.questionListService.getMyQuestionLists( + userId, + query + ); return res.send({ success: true, message: "My question lists received successfully.", data: { myQuestionLists, + meta, }, }); } catch (error) { @@ -161,16 +186,21 @@ export class QuestionListController { @Get("scrap") @UseGuards(AuthGuard("jwt")) - async getScrappedQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { + async getScrappedQuestionLists( + @Query() query: PaginateQuery, + @Res() res, + @JwtPayload() token: IJwtPayload + ) { try { const userId = token.userId; - const scrappedQuestionLists = - await this.questionListService.getScrappedQuestionLists(userId); + const { scrappedQuestionLists, meta } = + await this.questionListService.getScrappedQuestionLists(userId, query); return res.send({ success: true, message: "Scrapped question lists received successfully.", data: { - scrappedQuestionLists, + questionList: scrappedQuestionLists, + meta, }, }); } catch (error) { @@ -201,7 +231,7 @@ export class QuestionListController { success: true, message: "Question list is scrapped successfully.", data: { - scrappedQuestionList, + questionList: scrappedQuestionList, }, }); } catch (error) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index 8d36f1ef..a8cb8e9d 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -4,6 +4,7 @@ import { QuestionList } from "./question-list.entity"; import { Question } from "./question.entity"; import { Category } from "./category.entity"; import { User } from "@/user/user.entity"; +import { PaginateQuery } from "nestjs-paginate"; @Injectable() export class QuestionListRepository { @@ -18,9 +19,10 @@ export class QuestionListRepository { } findPublicQuestionLists() { - return this.dataSource.getRepository(QuestionList).find({ - where: { isPublic: true }, - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .where("question_list.is_public = :isPublic", { isPublic: true }); } async getCategoryIdByName(categoryName: string) { @@ -33,13 +35,12 @@ export class QuestionListRepository { } findPublicQuestionListsByCategoryId(categoryId: number) { - return this.dataSource.getRepository(QuestionList).find({ - where: { - isPublic: true, - categories: { id: categoryId }, - }, - relations: ["categories"], - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .innerJoin("question_list.categories", "category") + .where("question_list.is_public = :isPublic", { isPublic: true }) + .andWhere("category.id = :categoryId", { categoryId }); } async findCategoryNamesByQuestionListId(questionListId: number) { @@ -66,9 +67,11 @@ export class QuestionListRepository { } getContentsByQuestionListId(questionListId: number) { - return this.dataSource.getRepository(Question).find({ - where: { questionListId }, - }); + return this.dataSource + .getRepository(Question) + .createQueryBuilder("question") + .where("question.question_list_id = :questionListId", { questionListId }) + .getMany(); } async getUsernameById(userId: number) { @@ -80,9 +83,10 @@ export class QuestionListRepository { } getQuestionListsByUserId(userId: number) { - return this.dataSource.getRepository(QuestionList).find({ - where: { userId }, - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .where("question_list.userId = :userId", { userId }); } getQuestionCountByQuestionListId(questionListId: number) { @@ -109,9 +113,11 @@ export class QuestionListRepository { } getScrappedQuestionListsByUser(user: User) { - return this.dataSource.getRepository(QuestionList).find({ - where: { scrappedByUsers: user }, - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .innerJoin("question_list.scrappedByUsers", "user") + .where("user.id = :userId", { userId: user.id }); } unscrapQuestionList(questionListId: number, userId: number) { diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 3d4279eb..756fe75a 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -8,6 +8,7 @@ import { MyQuestionListDto } from "./dto/my-question-list.dto"; import { Question } from "./question.entity"; import { Transactional } from "typeorm-transactional"; import { QuestionList } from "@/question-list/question-list.entity"; +import { paginate, PaginateQuery } from "nestjs-paginate"; @Injectable() export class QuestionListService { @@ -16,12 +17,16 @@ export class QuestionListService { private readonly userRepository: UserRepository ) {} - async getAllQuestionLists() { + async getAllQuestionLists(query: PaginateQuery) { const allQuestionLists: GetAllQuestionListDto[] = []; const publicQuestionLists = await this.questionListRepository.findPublicQuestionLists(); + const result = await paginate(query, publicQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); - for (const publicQuestionList of publicQuestionLists) { + for (const publicQuestionList of result.data) { const { id, title, usage } = publicQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); @@ -38,22 +43,26 @@ export class QuestionListService { }; allQuestionLists.push(questionList); } - return allQuestionLists; + return { allQuestionLists, meta: result.meta }; } - async getAllQuestionListsByCategoryName(categoryName: string) { + async getAllQuestionListsByCategoryName(categoryName: string, query: PaginateQuery) { const allQuestionLists: GetAllQuestionListDto[] = []; const categoryId = await this.questionListRepository.getCategoryIdByName(categoryName); if (!categoryId) { - return []; + return {}; } const publicQuestionLists = await this.questionListRepository.findPublicQuestionListsByCategoryId(categoryId); + const result = await paginate(query, publicQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); - for (const publicQuestionList of publicQuestionLists) { + for (const publicQuestionList of result.data) { const { id, title, usage } = publicQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); @@ -70,7 +79,7 @@ export class QuestionListService { }; allQuestionLists.push(questionList); } - return allQuestionLists; + return { allQuestionLists, meta: result.meta }; } // 질문 생성 메서드 @@ -107,7 +116,10 @@ export class QuestionListService { async getQuestionListContents(questionListId: number) { const questionList = await this.questionListRepository.getQuestionListById(questionListId); - const { id, title, usage, userId } = questionList; + const { id, title, usage, isPublic, userId } = questionList; + if (!isPublic) { + throw new Error("This is private question list."); + } const contents = await this.questionListRepository.getContentsByQuestionListId(questionListId); @@ -116,7 +128,6 @@ export class QuestionListService { await this.questionListRepository.findCategoryNamesByQuestionListId(questionListId); const username = await this.questionListRepository.getUsernameById(userId); - const questionListContents: QuestionListContentsDto = { id, title, @@ -129,28 +140,29 @@ export class QuestionListService { return questionListContents; } - async getMyQuestionLists(userId: number) { + async getMyQuestionLists(userId: number, query: PaginateQuery) { const questionLists = await this.questionListRepository.getQuestionListsByUserId(userId); + const result = await paginate(query, questionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); const myQuestionLists: MyQuestionListDto[] = []; - for (const myQuestionList of questionLists) { + for (const myQuestionList of result.data) { const { id, title, isPublic, usage } = myQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); - const contents = await this.questionListRepository.getContentsByQuestionListId(id); - const questionList: MyQuestionListDto = { id, title, - contents, categoryNames, isPublic, usage, }; myQuestionLists.push(questionList); } - return myQuestionLists; + return { myQuestionLists, meta: result.meta }; } async findCategoriesByNames(categoryNames: string[]) { @@ -163,9 +175,15 @@ export class QuestionListService { return categories; } - async getScrappedQuestionLists(userId: number) { + async getScrappedQuestionLists(userId: number, query: PaginateQuery) { const user = await this.userRepository.getUserByUserId(userId); - return await this.questionListRepository.getScrappedQuestionListsByUser(user); + const scrappedQuestionLists = + await this.questionListRepository.getScrappedQuestionListsByUser(user); + const result = await paginate(query, scrappedQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); + return { scrappedQuestionLists: result.data, meta: result.meta }; } async scrapQuestionList(questionListId: number, userId: number) { @@ -176,13 +194,16 @@ export class QuestionListService { if (!questionList) throw new Error("Question list not found."); // 스크랩하려는 질문지가 내가 만든 질문지인지 확인 - const myQuestionLists = await this.questionListRepository.getQuestionListsByUserId(userId); + const myQuestionLists = await this.questionListRepository + .getQuestionListsByUserId(userId) + .getMany(); const isMyQuestionList = myQuestionLists.some((list) => list.id === questionListId); if (isMyQuestionList) throw new Error("Can't scrap my question list."); // 스크랩하려는 질문지가 이미 스크랩한 질문지인지 확인 - const alreadyScrappedQuestionLists = - await this.questionListRepository.getScrappedQuestionListsByUser(user); + const alreadyScrappedQuestionLists = await this.questionListRepository + .getScrappedQuestionListsByUser(user) + .getMany(); const isAlreadyScrapped = alreadyScrappedQuestionLists.some( (list) => list.id === questionListId );