From ec1f1dad2aa3b49ea5d6fe8233716cb833218ae2 Mon Sep 17 00:00:00 2001 From: DongHyeonWon Date: Mon, 30 Sep 2024 20:10:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20api=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20item=20isSelected=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: deleteComment body에서 params로 가져오는 방식 변경 * feat: 댓글 삭제 api 추가 * feat: item isSelected 추가 --- backend/src/comment/comment.controller.ts | 18 ++++++--- backend/src/comment/comment.service.ts | 8 +--- backend/src/game/game.controller.ts | 22 ++++++++++- backend/src/game/game.module.ts | 6 ++- backend/src/game/game.service.ts | 37 ++++++++++++++++--- backend/src/item/dto/item.dto.ts | 4 ++ frontend/src/apis/deleteComment.js | 7 ++++ .../components/comments/comment/Comment.jsx | 14 +++---- frontend/src/constants/api.js | 1 + .../src/hooks/queries/useDeleteComment.js | 22 +++++++++++ 10 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 frontend/src/apis/deleteComment.js create mode 100644 frontend/src/hooks/queries/useDeleteComment.js diff --git a/backend/src/comment/comment.controller.ts b/backend/src/comment/comment.controller.ts index cee89dd..77d3a44 100644 --- a/backend/src/comment/comment.controller.ts +++ b/backend/src/comment/comment.controller.ts @@ -1,9 +1,16 @@ -import { Body, Controller, Delete, Post, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { CommentService } from './comment.service'; import { CreateCommentDto } from 'src/comment/dto/create-comment.dto'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; -import { DeleteCommentDto } from 'src/comment/dto/delete-comment.dto'; import { ApiBody, ApiCookieAuth, @@ -39,15 +46,14 @@ export class CommentController { } @UseGuards(AuthGuard('jwt')) - @Delete() + @Delete(':comment_id') @ApiCookieAuth('accessToken') @ApiOperation({ summary: '아이템에 댓글을 삭제합니다.' }) - @ApiBody({ type: DeleteCommentDto }) async deleteComment( @Req() req: Request, - @Body() deleteCommentDto: DeleteCommentDto, + @Param('comment_id') comment_id: string, ): Promise { const kakaoId = req.user.kakaoId; - return this.commentService.deleteComment(kakaoId, deleteCommentDto); + return this.commentService.deleteComment(kakaoId, comment_id); } } diff --git a/backend/src/comment/comment.service.ts b/backend/src/comment/comment.service.ts index 2f00789..9d8d4be 100644 --- a/backend/src/comment/comment.service.ts +++ b/backend/src/comment/comment.service.ts @@ -7,7 +7,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Comment } from 'src/comment/comment.entity'; import { CommentDto } from 'src/comment/dto/comment.dto'; import { CreateCommentDto } from 'src/comment/dto/create-comment.dto'; -import { DeleteCommentDto } from 'src/comment/dto/delete-comment.dto'; import { Item } from 'src/item/item.entity'; import { User } from 'src/user/user.entity'; import { Repository } from 'typeorm'; @@ -154,12 +153,7 @@ export class CommentService { await this.commentsRepository.save(comment); } - async deleteComment( - userId: string, - deleteCommentDto: DeleteCommentDto, - ): Promise { - const { comment_id } = deleteCommentDto; - + async deleteComment(userId: string, comment_id: string): Promise { const comment = await this.commentsRepository.findOne({ where: { comment_id, user: { user_id: userId } }, }); diff --git a/backend/src/game/game.controller.ts b/backend/src/game/game.controller.ts index 4b29be2..3f402f8 100644 --- a/backend/src/game/game.controller.ts +++ b/backend/src/game/game.controller.ts @@ -23,12 +23,16 @@ import { } from '@nestjs/swagger'; import { GameResponseDto } from 'src/game/dto/gameResponse.dto'; import { ItemsResponseDto } from 'src/item/dto/itemsResponse.dto'; +import { JwtService } from '@nestjs/jwt'; @Controller('game') @ApiTags('게임 api') export class GameController { private logger = new Logger('GameController'); - constructor(private readonly gameService: GameService) {} + constructor( + private readonly gameService: GameService, + private readonly jwtService: JwtService, + ) {} @Get() @ApiOperation({ summary: '모든 게임들을 불러옵니다.' }) @@ -53,9 +57,23 @@ export class GameController { type: ItemsResponseDto, }) async getItemsByGameId( + @Req() req: Request, @Param('game_id') game_id: string, ): Promise { - return this.gameService.findItemsByGameId(game_id); + const token = req.cookies['accessToken']; // 쿠키에서 accessToken 읽기 + let user_id: string | null = null; + if (token) { + try { + const decoded = this.jwtService.verify(token, { + secret: process.env.JWT_SECRET, + }); // 토큰 검증 및 디코딩 + user_id = decoded.userId; // user_id 추출 + } catch (error) { + // 토큰 검증 실패 시 userId를 null로 유지하고 계속 진행 + console.log('유효하지 않은 토큰입니다.', error.message); + } + } + return this.gameService.findItemsByGameId(game_id, user_id); } @UseGuards(AuthGuard('jwt')) diff --git a/backend/src/game/game.module.ts b/backend/src/game/game.module.ts index 061214f..ad5d6d9 100644 --- a/backend/src/game/game.module.ts +++ b/backend/src/game/game.module.ts @@ -6,10 +6,12 @@ import { Game } from 'src/game/game.entity'; import { Item } from 'src/item/item.entity'; import { User } from 'src/user/user.entity'; import { JwtStrategy } from 'src/auth/strategies/jwt.strategy'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; +import { JwtService } from '@nestjs/jwt'; @Module({ - imports: [TypeOrmModule.forFeature([Game, User, Item])], + imports: [TypeOrmModule.forFeature([Game, User, Item, SelectedItem])], controllers: [GameController], - providers: [GameService, JwtStrategy], + providers: [GameService, JwtStrategy, JwtService], }) export class GameModule {} diff --git a/backend/src/game/game.service.ts b/backend/src/game/game.service.ts index 65d9562..00a29bd 100644 --- a/backend/src/game/game.service.ts +++ b/backend/src/game/game.service.ts @@ -7,6 +7,7 @@ import { Game } from 'src/game/game.entity'; import { ItemDto } from 'src/item/dto/item.dto'; import { ItemsResponseDto } from 'src/item/dto/itemsResponse.dto'; import { Item } from 'src/item/item.entity'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; import { User } from 'src/user/user.entity'; import { Repository } from 'typeorm'; @@ -19,6 +20,8 @@ export class GameService { private usersRepository: Repository, @InjectRepository(Item) private itemsRepository: Repository, + @InjectRepository(SelectedItem) + private selectedItemsRepository: Repository, ) {} async findAll({ @@ -52,22 +55,45 @@ export class GameService { return this.gamesRepository.findOneBy({ game_id: gameId }); } - async findItemsByGameId(game_id: string): Promise { + async findItemsByGameId( + game_id: string, + user_id: string | null, + ): Promise { // Game 엔티티에서 game_id를 기준으로 Item들을 가져옵니다. const items = await this.itemsRepository .createQueryBuilder('item') .leftJoinAndSelect('item.game', 'game') .where('game.game_id = :game_id', { game_id }) .getMany(); - if (items.length === 0) { throw new Error('No items found for the given game_id'); } + + if (user_id === null) { + const [firstItem, secondItem] = items; + + const response = new ItemsResponseDto(); + response.firstItem = this.toItemDto(firstItem, false); // 항상 false + response.secondItem = this.toItemDto(secondItem, false); // 항상 false + return response; + } + + const selectedItem = await this.selectedItemsRepository.findOne({ + where: { user: { user_id }, game: { game_id } }, + relations: ['item'], + }); + const [firstItem, secondItem] = items; const response = new ItemsResponseDto(); - response.firstItem = this.toItemDto(firstItem); - response.secondItem = this.toItemDto(secondItem); + response.firstItem = this.toItemDto( + firstItem, + selectedItem?.item.item_id === firstItem.item_id, + ); + response.secondItem = this.toItemDto( + secondItem, + selectedItem?.item.item_id === secondItem.item_id, + ); return response; } @@ -139,11 +165,12 @@ export class GameService { await this.gamesRepository.remove(game); } - private toItemDto(item: Item): ItemDto { + private toItemDto(item: Item, isSelected: boolean): ItemDto { const itemDto = new ItemDto(); itemDto.item_id = item.item_id; itemDto.item_text = item.item_text; itemDto.game_id = item.game.game_id; + itemDto.isSelected = isSelected; itemDto.selected_count = item.comments ? item.comments.length : 0; return itemDto; } diff --git a/backend/src/item/dto/item.dto.ts b/backend/src/item/dto/item.dto.ts index 5c65227..0e6d2f1 100644 --- a/backend/src/item/dto/item.dto.ts +++ b/backend/src/item/dto/item.dto.ts @@ -17,4 +17,8 @@ export class ItemDto { @IsNotEmpty() @ApiProperty({ description: 'The number of comments on the item' }) selected_count: number; + + @IsNotEmpty() + @ApiProperty({ description: 'The boolean of user selected' }) + isSelected: boolean; } diff --git a/frontend/src/apis/deleteComment.js b/frontend/src/apis/deleteComment.js new file mode 100644 index 0000000..bb6606c --- /dev/null +++ b/frontend/src/apis/deleteComment.js @@ -0,0 +1,7 @@ +import { axiosInstance } from '.'; +import END_POINTS from '../constants/api'; + +export default async function deleteComment(commentId) { + const { data } = await axiosInstance.delete(END_POINTS.DELETE_COMMENT(commentId)); + return data; +} diff --git a/frontend/src/components/comments/comment/Comment.jsx b/frontend/src/components/comments/comment/Comment.jsx index ad39cb8..29a2134 100644 --- a/frontend/src/components/comments/comment/Comment.jsx +++ b/frontend/src/components/comments/comment/Comment.jsx @@ -4,10 +4,12 @@ import HeartIconImg from '../../common/icon/HeartIcon'; import timeAgo from '../../../utils/timeAgo'; import useBestCommentLike from '../../../hooks/queries/useBestCommentLike'; import useCommentLike from '../../../hooks/queries/useCommentLike'; +import useDeleteComment from '../../../hooks/queries/useDeleteComment'; export default function Comment(props) { const { mutate: bestCommentLikeMutate } = useBestCommentLike(props.commentId, props.itemId); const { mutate: commentLikeMutate } = useCommentLike(props.commentId, props.itemId); + const { mutate: deleteCommentMutate } = useDeleteComment(props.commentId, props.itemId); const handleClick = () => { if (props.isBest) { bestCommentLikeMutate({ isHeart: props.isHeart }); @@ -18,13 +20,9 @@ export default function Comment(props) { } }; - // const handleLike = () => { - // if (isHeart) { - // //좋아요 취소 - // return; - // } - // //좋아요 - // }; + const handleDeleteComment = () => { + deleteCommentMutate(); + }; return ( @@ -34,7 +32,7 @@ export default function Comment(props) { {props.nickname} {timeAgo(new Date(props.time))} - + diff --git a/frontend/src/constants/api.js b/frontend/src/constants/api.js index ff2348b..371b3d7 100644 --- a/frontend/src/constants/api.js +++ b/frontend/src/constants/api.js @@ -11,6 +11,7 @@ const END_POINTS = { CREATE_COMMENT: '/comment', GET_MY_GAMES: (page, limit) => `/game/user?page=${page}&limit=${limit}`, DELETE_MY_GAMES: (gameId) => `/game/${gameId}`, + DELETE_COMMENT: (commentId) => `/comment/${commentId}`, }; export default END_POINTS; diff --git a/frontend/src/hooks/queries/useDeleteComment.js b/frontend/src/hooks/queries/useDeleteComment.js new file mode 100644 index 0000000..f6092c5 --- /dev/null +++ b/frontend/src/hooks/queries/useDeleteComment.js @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import deleteComment from '../../apis/deleteComment'; +import QUERY_KEYS from '../../constants/queryKeys'; +import { toast } from 'react-toastify'; + +export default function useDeleteComment(commentId, itemId) { + const queryClient = useQueryClient(); + const itemIdNumber = parseInt(itemId); + return useMutation({ + mutationFn: async () => { + return await deleteComment(commentId); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUERY_KEYS.COMMENTS, { itemId: itemIdNumber }]); + queryClient.invalidateQueries([QUERY_KEYS.BEST_COMMENTS, { itemId: itemIdNumber }]); + toast.success('댓글이 삭제되었습니다.'); + }, + onError: () => { + toast.error('댓글 삭제에 실패했습니다.'); + }, + }); +}