From 505ae5225d65e2481a3f8c00367587364ac8a133 Mon Sep 17 00:00:00 2001 From: DongHyeonWon Date: Mon, 30 Sep 2024 23:04:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20api=20=EC=B6=94=EA=B0=80=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: item select count 0으로 나오는 버그 수정 * feat: 아이템 선택 api 추가 * feat: 게임 만들때 text 빈값일때 에러처리 --- backend/src/game/game.service.ts | 6 +- frontend/src/apis/selectItem.js | 7 ++ frontend/src/components/choice/Choice.jsx | 39 +++++++- .../src/components/choice/Choice.styled.js | 55 +++++++++-- .../components/comments/comment/Comment.jsx | 2 +- .../src/components/itemcreate/ItemCreate.jsx | 1 + frontend/src/constants/api.js | 1 + frontend/src/hooks/queries/useSelectItem.js | 97 +++++++++++++++++++ frontend/src/styles/theme.js | 3 + frontend/src/utils/calculatePercent.js | 3 + 10 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 frontend/src/apis/selectItem.js create mode 100644 frontend/src/hooks/queries/useSelectItem.js create mode 100644 frontend/src/utils/calculatePercent.js diff --git a/backend/src/game/game.service.ts b/backend/src/game/game.service.ts index 00a29bd..c59d111 100644 --- a/backend/src/game/game.service.ts +++ b/backend/src/game/game.service.ts @@ -104,6 +104,10 @@ export class GameService { ): Promise { const { firstItemText, secondItemText } = createGameDto; + if (!firstItemText || !secondItemText) { + throw new Error('게임을 생성하기 위해서는 두 개의 아이템이 필요합니다.'); + } + // 1. 사용자 조회 const user = await this.usersRepository.findOneBy({ user_id: userId }); if (!user) { @@ -171,7 +175,7 @@ export class GameService { 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; + itemDto.selected_count = item.selected_count; return itemDto; } diff --git a/frontend/src/apis/selectItem.js b/frontend/src/apis/selectItem.js new file mode 100644 index 0000000..ace99de --- /dev/null +++ b/frontend/src/apis/selectItem.js @@ -0,0 +1,7 @@ +import { axiosInstance } from '.'; +import END_POINTS from '../constants/api'; + +export default async function selectItem(gameId, itemId) { + const { data } = await axiosInstance.post(END_POINTS.SELECT_ITEM(gameId, itemId)); + return data; +} diff --git a/frontend/src/components/choice/Choice.jsx b/frontend/src/components/choice/Choice.jsx index f390e8e..0a3680a 100644 --- a/frontend/src/components/choice/Choice.jsx +++ b/frontend/src/components/choice/Choice.jsx @@ -1,11 +1,46 @@ +import useSelectItem from '../../hooks/queries/useSelectItem'; +import calculatePercent from '../../utils/calculatePercent'; import * as S from './Choice.styled'; export default function Choice(props) { const { game } = props; + console.log(game?.firstItem.item_id); + const { mutate: selectItemMutate } = useSelectItem(game?.firstItem.game_id); + const isColored = (firstItemSelected, secondItemSelected) => { + return !firstItemSelected && !secondItemSelected ? true : firstItemSelected ? true : false; + }; + return ( - {game?.firstItem.item_text} - {game?.secondItem.item_text} + selectItemMutate(game?.firstItem?.item_id)} + iscolored={isColored(game?.firstItem.isSelected, game?.secondItem.isSelected)} + > + {game?.firstItem.item_text} + + + selectItemMutate(game?.secondItem?.item_id)} + iscolored={isColored(game?.secondItem.isSelected, game?.firstItem.isSelected)} + > + {game?.secondItem.item_text} + + ); } diff --git a/frontend/src/components/choice/Choice.styled.js b/frontend/src/components/choice/Choice.styled.js index 57a9a6b..225578e 100644 --- a/frontend/src/components/choice/Choice.styled.js +++ b/frontend/src/components/choice/Choice.styled.js @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; export const ItemContainer = styled.div` width: 100%; @@ -12,8 +12,9 @@ export const ItemContainer = styled.div` `; export const ItemList = styled.div` + position: relative; word-break: keep-all; - width: 100%; + width: 50%; padding: 5rem; text-align: center; display: flex; @@ -25,12 +26,15 @@ export const ItemList = styled.div` border-top: 0.2rem solid ${(props) => props.theme.colors.black}; border-left: 0.2rem solid ${(props) => props.theme.colors.black}; border-bottom: 0.2rem solid ${(props) => props.theme.colors.black}; - background-color: ${(props) => props.theme.colors.primaryBlue}; + border-right: 0.1rem solid ${(props) => props.theme.colors.black}; + background-color: ${(props) => + props.iscolored ? props.theme.colors.primaryBlue : props.theme.colors.gray500}; `; export const ItemList2 = styled.div` + position: relative; word-break: keep-all; - width: 100%; + width: 50%; padding: 5rem; text-align: center; display: flex; @@ -39,6 +43,45 @@ export const ItemList2 = styled.div` border-top-right-radius: 2rem; /* 오른쪽 위 */ font-size: 3.4rem; height: 100%; - border: 0.2rem solid ${(props) => props.theme.colors.black}; - background-color: ${(props) => props.theme.colors.primaryGreen}; + border-top: 0.2rem solid ${(props) => props.theme.colors.black}; + border-bottom: 0.2rem solid ${(props) => props.theme.colors.black}; + border-right: 0.2rem solid ${(props) => props.theme.colors.black}; + border-left: 0.1rem solid ${(props) => props.theme.colors.black}; + background-color: ${(props) => + props.iscolored ? props.theme.colors.primaryGreen : props.theme.colors.gray500}; + z-index: 0; +`; + +export const itemText = styled.div` + font-size: 3.4rem; + position: absolute; + z-index: 2; +`; + +// keyframes을 동적으로 생성 +const growHeight = (percent) => keyframes` + 0% { + height: 0; + } + 100% { + height: ${percent}%; + } +`; + +export const percentBox = styled.div` + display: ${(props) => (props.isshow ? 'block' : 'none')}; + background-color: ${(props) => + props.first + ? props.isselected + ? props.theme.colors.percentBoxBlue + : props.theme.colors.gray350 + : props.isselected + ? props.theme.colors.percentBoxGreen + : props.theme.colors.gray350}; + width: 100%; + bottom: 0; + position: absolute; + z-index: 1; + border-radius: ${(props) => (props.first ? '2rem 0 0 0' : '0 2rem 0 0')}; + animation: ${(props) => growHeight(props.percent)} 0.7s ease-in-out forwards; `; diff --git a/frontend/src/components/comments/comment/Comment.jsx b/frontend/src/components/comments/comment/Comment.jsx index 29a2134..6d0d8d2 100644 --- a/frontend/src/components/comments/comment/Comment.jsx +++ b/frontend/src/components/comments/comment/Comment.jsx @@ -14,7 +14,7 @@ export default function Comment(props) { if (props.isBest) { bestCommentLikeMutate({ isHeart: props.isHeart }); return; - } else { + } else { commentLikeMutate({ isHeart: props.isHeart }); return; } diff --git a/frontend/src/components/itemcreate/ItemCreate.jsx b/frontend/src/components/itemcreate/ItemCreate.jsx index 59da80c..ef18640 100644 --- a/frontend/src/components/itemcreate/ItemCreate.jsx +++ b/frontend/src/components/itemcreate/ItemCreate.jsx @@ -23,6 +23,7 @@ export default function ItemCreate() { const handleOnclick = () => { const firstItemText = textarea1Ref.current.value; const secondItemText = textarea2Ref.current.value; + mutate({ firstItemText, secondItemText }); }; return ( diff --git a/frontend/src/constants/api.js b/frontend/src/constants/api.js index 371b3d7..23f4365 100644 --- a/frontend/src/constants/api.js +++ b/frontend/src/constants/api.js @@ -12,6 +12,7 @@ const END_POINTS = { GET_MY_GAMES: (page, limit) => `/game/user?page=${page}&limit=${limit}`, DELETE_MY_GAMES: (gameId) => `/game/${gameId}`, DELETE_COMMENT: (commentId) => `/comment/${commentId}`, + SELECT_ITEM: (gameId, itemId) => `/games/${gameId}/items/${itemId}/select`, }; export default END_POINTS; diff --git a/frontend/src/hooks/queries/useSelectItem.js b/frontend/src/hooks/queries/useSelectItem.js new file mode 100644 index 0000000..059299a --- /dev/null +++ b/frontend/src/hooks/queries/useSelectItem.js @@ -0,0 +1,97 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import selectItem from '../../apis/selectItem'; +import QUERY_KEYS from '../../constants/queryKeys'; +import { toast } from 'react-toastify'; + +export default function useSelectItem(gameId) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (itemId) => { + const response = await selectItem(gameId, itemId); + return response; + }, + onMutate: async (itemId) => { + await queryClient.cancelQueries([QUERY_KEYS.GAMES_ITEMS]); + + const previousData = queryClient.getQueryData([QUERY_KEYS.GAMES_ITEMS]); + + queryClient.setQueryData([QUERY_KEYS.GAMES_ITEMS], (oldData) => { + if (!oldData) return oldData; + + const firstItemSelected = oldData.firstItem.item_id === itemId; + const secondItemSelected = oldData.secondItem.item_id === itemId; + + // 이미 선택된 아이템을 다시 눌러서 선택을 취소하는 경우 + if (firstItemSelected && oldData.firstItem.isSelected) { + return { + ...oldData, + firstItem: { + ...oldData.firstItem, + isSelected: false, + selected_count: oldData.firstItem.selected_count - 1, + }, + }; + } + + if (secondItemSelected && oldData.secondItem.isSelected) { + return { + ...oldData, + secondItem: { + ...oldData.secondItem, + isSelected: false, + selected_count: oldData.secondItem.selected_count - 1, + }, + }; + } + + // 반대쪽 아이템을 선택했을 때 기존 선택한 아이템의 선택 해제 및 카운트 감소 + if (firstItemSelected && !oldData.firstItem.isSelected) { + return { + ...oldData, + firstItem: { + ...oldData.firstItem, + isSelected: true, + selected_count: oldData.firstItem.selected_count + 1, + }, + secondItem: oldData.secondItem.isSelected + ? { + ...oldData.secondItem, + isSelected: false, + selected_count: oldData.secondItem.selected_count - 1, + } + : oldData.secondItem, + }; + } + + if (secondItemSelected && !oldData.secondItem.isSelected) { + return { + ...oldData, + secondItem: { + ...oldData.secondItem, + isSelected: true, + selected_count: oldData.secondItem.selected_count + 1, + }, + firstItem: oldData.firstItem.isSelected + ? { + ...oldData.firstItem, + isSelected: false, + selected_count: oldData.firstItem.selected_count - 1, + } + : oldData.firstItem, + }; + } + + return oldData; + }); + + return { previousData }; + }, + + onError: (err, variables, context) => { + console.log(err); + toast.error('다시 시도해주세요.'); + queryClient.setQueryData([QUERY_KEYS.GAMES_ITEMS], context.previousData); + }, + }); +} diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js index b74696d..ffc0b2e 100644 --- a/frontend/src/styles/theme.js +++ b/frontend/src/styles/theme.js @@ -7,10 +7,13 @@ export const theme = { gray350: '#C8C8C8', gray400: '#444444', gray450: '#A7A7A7', + gray500: '#D9D9D9', black: '#080808', primaryBlue: '#4DDDFF', primaryGreen: '#64F0A0', primaryRed: '#F34343', + percentBoxBlue: '#11CEFA', + percentBoxGreen: '#37EF85', }, }; diff --git a/frontend/src/utils/calculatePercent.js b/frontend/src/utils/calculatePercent.js new file mode 100644 index 0000000..89d50c2 --- /dev/null +++ b/frontend/src/utils/calculatePercent.js @@ -0,0 +1,3 @@ +export default function calculatePercent(value, total) { + return total > 0 ? ((value / total) * 100).toFixed(0) : 0; +}