diff --git a/backend/src/question-list/dto/get-all-question-list.dto.ts b/backend/src/question-list/dto/get-all-question-list.dto.ts index 9d87dfa..d9653f4 100644 --- a/backend/src/question-list/dto/get-all-question-list.dto.ts +++ b/backend/src/question-list/dto/get-all-question-list.dto.ts @@ -4,4 +4,5 @@ export interface GetAllQuestionListDto { categoryNames: string[]; usage: number; questionCount: number; + isScrap: boolean; } diff --git a/backend/src/question-list/dto/question-list-contents.dto.ts b/backend/src/question-list/dto/question-list-contents.dto.ts index 3be38c9..ad4b37d 100644 --- a/backend/src/question-list/dto/question-list-contents.dto.ts +++ b/backend/src/question-list/dto/question-list-contents.dto.ts @@ -7,4 +7,6 @@ export interface QuestionListContentsDto { contents: Question[]; usage: number; username: string; + isScrap: boolean; + scrapCount: number; } diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index daf7da1..c70d63a 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -3,11 +3,13 @@ import { Controller, Delete, Get, + HttpStatus, Param, Patch, Post, Query, Req, + Res, UseGuards, UsePipes, ValidationPipe, @@ -28,31 +30,42 @@ export class QuestionListController { constructor(private readonly questionListService: QuestionListService) {} @Get() + @UseGuards(AuthGuard("jwt")) @UsePipes(new ValidationPipe({ transform: true })) - async getAllQuestionLists(@Query() query: PaginateQueryDto) { + async getAllQuestionLists( + @Res() res, + @Query() query: PaginateQueryDto, + @JwtPayload() token: IJwtPayload + ) { try { - const { allQuestionLists, meta } = - await this.questionListService.getAllQuestionLists(query); - return { + const userId = token ? token.userId : null; + const { allQuestionLists, meta } = await this.questionListService.getAllQuestionLists( + query, + userId + ); + return res.status(HttpStatus.OK).json({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, meta, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to get all question lists.", error: error.message, - }; + }); } } @Post("category") + @UseGuards(AuthGuard("jwt")) @UsePipes(new ValidationPipe({ transform: true })) async getAllQuestionListsByCategoryName( + @Res() res, + @JwtPayload() token: IJwtPayload, @Query() query: PaginateQueryDto, @Body() body: { @@ -60,24 +73,29 @@ export class QuestionListController { } ) { try { + const userId = token ? token.userId : null; + const { categoryName } = body; query.category = categoryName; - const { allQuestionLists, meta } = - await this.questionListService.getAllQuestionLists(query); - return { + + const { allQuestionLists, meta } = await this.questionListService.getAllQuestionLists( + query, + userId + ); + return res.status(HttpStatus.OK).json({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, meta, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to get all question lists.", error: error.message, - }; + }); } } @@ -85,6 +103,7 @@ export class QuestionListController { @UseGuards(AuthGuard("jwt")) async createQuestionList( @JwtPayload() token: IJwtPayload, + @Res() res, @Req() req, @Body() body: { @@ -96,6 +115,13 @@ export class QuestionListController { ) { try { const { title, contents, categoryNames, isPublic } = body; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); // 질문지 DTO 준비 const createQuestionListDto: CreateQuestionListDto = { @@ -103,32 +129,33 @@ export class QuestionListController { contents, categoryNames, isPublic, - userId: token.userId, + userId, }; // 질문지 생성 const { createdQuestionList, createdQuestions } = await this.questionListService.createQuestionList(createQuestionListDto); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list created successfully.", data: { createdQuestionList, createdQuestions, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to create question list.", error: error.message, - }; + }); } } @Post("contents") @UseGuards(AuthGuard("jwt")) async getQuestionListContents( + @Res() res, @JwtPayload() token: IJwtPayload, @Body() body: { @@ -136,62 +163,82 @@ export class QuestionListController { } ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + const { questionListId } = body; const questionListContents: QuestionListContentsDto = await this.questionListService.getQuestionListContents(questionListId, userId); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list contents received successfully.", data: { questionListContents, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to get question list contents.", error: error.message, - }; + }); } } @Get("my") @UseGuards(AuthGuard("jwt")) @UsePipes(new ValidationPipe({ transform: true })) - async getMyQuestionLists(@Query() query: PaginateQueryDto, @JwtPayload() token: IJwtPayload) { + async getMyQuestionLists( + @Res() res, + @Query() query: PaginateQueryDto, + @JwtPayload() token: IJwtPayload + ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { myQuestionLists, meta } = await this.questionListService.getMyQuestionLists( userId, query ); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "My question lists received successfully.", data: { myQuestionLists, meta, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to get my question lists.", error: error.message, - }; + }); } } @Patch("/:questionListId") @UseGuards(AuthGuard("jwt")) async updateQuestionList( + @Res() res, @JwtPayload() token: IJwtPayload, @Param("questionListId") questionListId: number, @Body() body: { title?: string; isPublic?: boolean; categoryNames?: string[] } ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { title, isPublic, categoryNames } = body; const updateQuestionListDto: UpdateQuestionListDto = { id: questionListId, @@ -203,64 +250,80 @@ export class QuestionListController { const updatedQuestionList = await this.questionListService.updateQuestionList(updateQuestionListDto); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list is updated successfully.", data: { questionList: updatedQuestionList, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to update question list.", error: error.message, - }; + }); } } @Delete("/:questionListId") @UseGuards(AuthGuard("jwt")) async deleteQuestionList( + @Res() res, @JwtPayload() token: IJwtPayload, @Param("questionListId") questionListId: number ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const result = await this.questionListService.deleteQuestionList( questionListId, userId ); if (result.affected) { - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list is deleted successfully.", - }; + }); } else { - return { - success: true, + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, message: "Failed to delete question list.", - }; + }); } } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: true, message: "Failed to delete question list.", error: error.message, - }; + }); } } @Post("/:questionListId/question") @UseGuards(AuthGuard("jwt")) async addQuestion( + @Res() res, @JwtPayload() token: IJwtPayload, @Body() body: { content: string }, @Param("questionListId") questionListId: number ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { content } = body; const questionDto: QuestionDto = { content, @@ -270,31 +333,39 @@ export class QuestionListController { const result = await this.questionListService.addQuestion(questionDto); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "The new question is added to the list successfully.", data: { questionList: result, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to add the new question to the list.", error: error.message, - }; + }); } } @Patch("/:questionListId/question/:questionId") @UseGuards(AuthGuard("jwt")) async updateQuestion( + @Res() res, @JwtPayload() token: IJwtPayload, @Body() body: { content: string }, @Param() params: { questionListId: number; questionId: number } ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { content } = body; const { questionListId, questionId } = params; const questionDto: QuestionDto = { @@ -306,30 +377,38 @@ export class QuestionListController { const result = await this.questionListService.updateQuestion(questionDto); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question is updated successfully.", data: { questionList: result, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to update question.", error: error.message, - }; + }); } } @Delete("/:questionListId/question/:questionId") @UseGuards(AuthGuard("jwt")) async deleteQuestion( + @Res() res, @JwtPayload() token: IJwtPayload, @Param() params: { questionListId: number; questionId: number } ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { questionListId, questionId } = params; const deleteQuestionDto: DeleteQuestionDto = { id: questionId, @@ -338,22 +417,22 @@ export class QuestionListController { }; const result = await this.questionListService.deleteQuestion(deleteQuestionDto); if (result.affected) { - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question is deleted successfully.", - }; + }); } else { - return { - success: true, + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, message: "Failed to delete question.", - }; + }); } } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to delete question.", error: error.message, - }; + }); } } @@ -361,90 +440,114 @@ export class QuestionListController { @UseGuards(AuthGuard("jwt")) @UsePipes(new ValidationPipe({ transform: true })) async getScrappedQuestionLists( + @Res() res, @Query() query: PaginateQueryDto, @JwtPayload() token: IJwtPayload ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { scrappedQuestionLists, meta } = await this.questionListService.getScrappedQuestionLists(userId, query); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Scrapped question lists received successfully.", data: { questionList: scrappedQuestionLists, meta, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to get scrapped question lists.", error: error.message, - }; + }); } } @Post("scrap") @UseGuards(AuthGuard("jwt")) async scrapQuestionList( + @Res() res, @JwtPayload() token: IJwtPayload, @Body() body: { questionListId: number } ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login required.", + }); + const { questionListId } = body; const scrappedQuestionList = await this.questionListService.scrapQuestionList( questionListId, userId ); - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list is scrapped successfully.", data: { questionList: scrappedQuestionList, }, - }; + }); } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to scrap question list.", error: error.message, - }; + }); } } @Delete("scrap/:questionListId") @UseGuards(AuthGuard("jwt")) async unscrapQuestionList( + @Res() res, @JwtPayload() token: IJwtPayload, @Param("questionListId") questionListId: number ) { try { - const userId = token.userId; + const userId = token ? token.userId : null; + + if (!userId) + return res.status(HttpStatus.UNAUTHORIZED).json({ + success: false, + message: "Login is required to create question list.", + }); + const unscrappedQuestionList = await this.questionListService.unscrapQuestionList( questionListId, userId ); if (unscrappedQuestionList.affected) { - return { + return res.status(HttpStatus.OK).json({ success: true, message: "Question list unscrapped successfully.", - }; + }); } else { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to unscrap question list.", - }; + }); } } catch (error) { - return { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ success: false, message: "Failed to unscrap question list.", error: error.message, - }; + }); } } } diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 1834f64..d98388a 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -28,7 +28,7 @@ export class QuestionListService { private readonly categoryRepository: CategoryRepository ) {} - async getAllQuestionLists(query: PaginateQueryDto) { + async getAllQuestionLists(query: PaginateQueryDto, userId: number) { const allQuestionLists: GetAllQuestionListDto[] = []; let categoryId = null; @@ -53,12 +53,17 @@ export class QuestionListService { where: { questionListId: id }, }); + const isScrap = userId + ? await this.questionListRepository.isQuestionListScrapped(id, userId) + : false; + const questionList: GetAllQuestionListDto = { id, title, categoryNames, usage, questionCount, + isScrap, }; allQuestionLists.push(questionList); } @@ -113,9 +118,14 @@ export class QuestionListService { const questionList = await this.questionListRepository.findOne({ where: { id: questionListId }, }); + const { id, title, usage, isPublic } = questionList; - if (!isPublic && questionList.userId !== userId) { - throw new Error("This is private question list."); + const authorId = questionList.userId; + + if (!isPublic) { + if (!userId || questionList.userId !== userId) { + throw new Error("This is a private question list."); + } } const contents = await this.questionRepository.getContentsByQuestionListId(questionListId); @@ -123,8 +133,15 @@ export class QuestionListService { const categoryNames = await this.categoryRepository.findCategoryNamesByQuestionListId(questionListId); - const user = await this.userRepository.getUserByUserId(userId); - const username = user.username; + const author = await this.userRepository.getUserByUserId(authorId); + const username = author.username; + + const isScrap = userId + ? await this.questionListRepository.isQuestionListScrapped(id, userId) + : false; + + const scrapCount = await this.questionListRepository.getScrapCount(id); + const questionListContents: QuestionListContentsDto = { id, title, @@ -132,6 +149,8 @@ export class QuestionListService { categoryNames, usage, username, + isScrap, + scrapCount: parseInt(scrapCount.count), }; return questionListContents; diff --git a/backend/src/question-list/repository/question-list.repository.ts b/backend/src/question-list/repository/question-list.repository.ts index ae16933..1d4834d 100644 --- a/backend/src/question-list/repository/question-list.repository.ts +++ b/backend/src/question-list/repository/question-list.repository.ts @@ -58,6 +58,24 @@ export class QuestionListRepository extends Repository { .execute(); } + isQuestionListScrapped(questionListId: number, userId: number) { + return this.createQueryBuilder("question_list") + .innerJoin("question_list.scrappedByUsers", "user") + .where("question_list.id = :questionListId", { questionListId }) + .andWhere("user.id = :userId", { userId }) + .select("1") + .getRawOne() + .then((result) => !!result); + } + + getScrapCount(questionListId: number) { + return this.createQueryBuilder("question_list") + .innerJoin("question_list.scrappedByUsers", "user") + .where("question_list.id = :questionListId", { questionListId }) + .select("COUNT(user.id)", "count") + .getRawOne(); + } + async paginate(paginateDto: PaginateDto) { const { queryBuilder, skip, take, field, direction } = paginateDto; return await queryBuilder