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] 라운드 통계 화면 및 게임 결과 화면 구현 #73

Merged
merged 28 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e61ea09
chore: 동적 파라미터 라우팅 설정 #50
rbgksqkr Jul 23, 2024
0128c04
refactor: BalanceContent 인터페이스 수정 #50
rbgksqkr Jul 23, 2024
752c497
feat: 라우팅 경로 수정 및 라운드 통계 분기처리 #50
rbgksqkr Jul 23, 2024
9650f21
feat: 라운드 통계 수정된 UI 반영 및 Tab UI 구현 #50
rbgksqkr Jul 23, 2024
91f40b3
refactor: 사용하지 않는 파일 삭제 #50
rbgksqkr Jul 23, 2024
c65abe3
chore: stylelint css 속성 추가 #50
rbgksqkr Jul 23, 2024
aa3b05a
feat: 숫자 카운팅 애니메이션 커스텀 훅 구현 #50
rbgksqkr Jul 24, 2024
a200136
feat: roundVoteResult 인터페이스 선언 및 msw 설정 #50
rbgksqkr Jul 24, 2024
be3f49a
refactor: 그룹 통계와 전체 통계 서버 상태를 react-query로 관리 #50
rbgksqkr Jul 24, 2024
7103261
refactor: 그룹 통계와 전체 통계 컴포넌트 분리 #50
rbgksqkr Jul 24, 2024
2d26903
refactor: Header 홈 아이콘 대신 현재 라운드 표시 #50
rbgksqkr Jul 24, 2024
a04c200
fix: Button disabled 설정 추가 #50
rbgksqkr Jul 24, 2024
d8a6d53
feat: 다음 라운드로 이동하는 API 통신 구현 #50
rbgksqkr Jul 24, 2024
f46054d
feat: 최종 게임 결과 불러오는 API 구현 #50
rbgksqkr Jul 24, 2024
090e2e4
feat: Header 분기처리 및 라운드 API 불러오기 #50
rbgksqkr Jul 24, 2024
27715e0
feat: 최종 결과 페이지 UI 퍼블리싱 및 API 연결 #50
rbgksqkr Jul 24, 2024
73c9f90
refactor: GameResultItem 컴포넌트 분리 #50
rbgksqkr Jul 24, 2024
93dd3c3
refactor: placeholderData를 이용하여 초기값을 설정하고, 라운드 결과 애니메이션 적용 #50
rbgksqkr Jul 24, 2024
76a259c
chore: console error eslint 룰 추가 #50
rbgksqkr Jul 25, 2024
6e4ec20
refactor: 다음 라운드로 넘어가는 버튼 컴포넌트 분리 #50
rbgksqkr Jul 25, 2024
0a65739
refactor: Tab 컴포넌트 분리 #50
rbgksqkr Jul 25, 2024
261aa4e
refactor: TabContentContainer 컴포넌트 분리 #50
rbgksqkr Jul 25, 2024
347cfb5
refactor: 게임 결과 페이지의 FinalButton 컴포넌트 분리 #50
rbgksqkr Jul 25, 2024
adbed42
design: 헤더 아이콘 크기 고정 및 버튼 패딩 제거 #50
rbgksqkr Jul 25, 2024
dfc98e4
refactor: 라우팅 경로 상수화 및 헤더 컴포넌트 분기 처리 #50
rbgksqkr Jul 25, 2024
7a2deb5
fix: 컴포넌트명과 타입명이 같아서 생기는 빌드 오류 해결 #50
rbgksqkr Jul 25, 2024
8defa35
style: 카운팅 애니메이션 주석 추가 #50
rbgksqkr Jul 25, 2024
04ffc25
refactor: 라운드 통계 화면 탭과 TabItem 컴포넌트명 수정 #50
rbgksqkr Jul 25, 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
1 change: 1 addition & 0 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"jest.polyfills.js"
],
"rules": {
"no-console": "error",
Copy link
Contributor

Choose a reason for hiding this comment

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

L5 - 참고 의견

no console 이거 흔히 하는 실수인데 방지하는 옵션 좋네요 굿

"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-unused-vars": "warn",
"react/no-unknown-property": ["error", { "ignore": ["css"] }],
Expand Down
10 changes: 8 additions & 2 deletions frontend/.stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
"bottom",
"left",
"z-index",
"flex",
"flex-basis",
"flex-direction",
"justify-content",
"align-items"
"align-items",
"gap"
]
},
{
Expand All @@ -49,7 +52,10 @@
"padding-right",
"padding-bottom",
"padding-left",
"border"
"border",
"border-radius",
"border-top",
"border-bottom"
]
},
{
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/apis/balanceContent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fetcher from './fetcher';

import { API_URL } from '@/constants/url';
import { BalanceContent } from '@/types/balanceContent';
import { BalanceContent, GameFinalResult } from '@/types/balanceContent';
import { RoundVoteResult } from '@/types/roundVoteResult';

export const fetchBalanceContent = async (roomId = 1): Promise<BalanceContent> => {
const res = await fetcher.get({ url: API_URL.balanceContent(roomId) });
Expand All @@ -24,3 +25,33 @@ export const voteBalanceContent = async (optionId: number, roomId = 1, contentId

return data;
};

export const fetchRoundVoteResult = async (roomId = 1, contentId = 1): Promise<RoundVoteResult> => {
const res = await fetcher.get({
url: API_URL.roundVoteResult(roomId, contentId),
});

const data = await res.json();

return data;
};

export const moveNextRound = async (roomId = 1): Promise<RoundVoteResult> => {
const res = await fetcher.post({
url: API_URL.moveNextRound(roomId),
});

const data = await res.json();

return data;
};

export const fetchFinalGameResult = async (roomId = 1): Promise<GameFinalResult[]> => {
const res = await fetcher.get({
url: API_URL.finalResult(roomId),
});

const data = await res.json();

return data;
};
14 changes: 14 additions & 0 deletions frontend/src/components/GameResult/GameResult.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';

import { fetchFinalGameResult } from '@/apis/balanceContent';

const useGameResultQuery = () => {
const gameResultQuery = useQuery({
queryKey: ['gameResult'],
queryFn: async () => await fetchFinalGameResult(),
});

return { ...gameResultQuery, gameResult: gameResultQuery.data };
};

export { useGameResultQuery };
20 changes: 12 additions & 8 deletions frontend/src/components/GameResult/GameResult.styled.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { css } from '@emotion/react';

import { Theme } from '@/styles/Theme';

export const gameResultLayout = css`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 40vh;
gap: 6rem;
gap: 4.8rem;
width: 100%;
`;

export const gameResultTitleWrapper = css`
display: flex;
flex-basis: 20%;
export const gameResultTitle = css`
${Theme.typography.slogan};
`;

export const gameResultTitle = css`
font-weight: bold;
font-size: 2.4rem;
export const rankListContainer = css`
display: flex;
flex-basis: 60%;
flex-direction: column;
gap: 2rem;
width: 100%;
`;
27 changes: 13 additions & 14 deletions frontend/src/components/GameResult/GameResult.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { useNavigate } from 'react-router-dom';

import { gameResultTitle, gameResultTitleWrapper, gameResultLayout } from './GameResult.styled';

import Button from '@/components/common/Button/Button';
import { useGameResultQuery } from './GameResult.hook';
import { gameResultTitle, gameResultLayout, rankListContainer } from './GameResult.styled';
import FinalButton from '../common/FinalButton/FinalButton';
import GameResultItem from '../GameResultItem/GameResultItem';

const GameResult = () => {
const navigate = useNavigate();

const goToHome = () => {
navigate('/');
};
const { gameResult } = useGameResultQuery();

return (
<div css={gameResultLayout}>
<div css={gameResultTitleWrapper}>
<>
<div css={gameResultLayout}>
<h1 css={gameResultTitle}>게임 결과</h1>
<div css={rankListContainer}>
{gameResult &&
gameResult.map((item) => <GameResultItem key={item.rank} gameFinalResult={item} />)}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

L4 - 변경 제안

게임 결과에서 매칭도가 순위대로 나열되는 것을 리스트로 볼 수 있을 것 같아서, div 대신에 ol 태그로 수정하는 것은 어떻게 생각하시나요? 😊

Copy link
Contributor Author

@rbgksqkr rbgksqkr Jul 27, 2024

Choose a reason for hiding this comment

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

오 좋습니다아 웹표준 고려하여 태그 사용하는 것 너무 좋네요 ㅎㅎ

</div>
<Button text="확인" onClick={goToHome} />
</div>
<FinalButton />
</>
);
};

Expand Down
42 changes: 42 additions & 0 deletions frontend/src/components/GameResultItem/GameResultItem.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { css } from '@emotion/react';

import { Theme } from '@/styles/Theme';

export const rankItem = css`
display: flex;
justify-content: space-between;
align-items: center;
${Theme.typography.headline3};
`;

export const rankInfoContainer = css`
display: flex;
flex-basis: 85%;
align-items: center;
gap: 1.2rem;
`;

export const rankNumber = css`
${Theme.typography.headline1}
`;

export const nicknameContainer = (percent: number) => css`
display: flex;
overflow: visible;
gap: 1rem;
width: ${percent > 5 ? percent - 5 : percent}%;
height: 100%;
padding: 1.2rem;
border-radius: ${Theme.borderRadius.radius20};

background-color: ${Theme.color.peanut400};
transition: all 2s;
`;

export const rankPercent = css`
${Theme.typography.headline3}
`;

export const nickname = css`
min-width: 5.6rem;
`;
36 changes: 36 additions & 0 deletions frontend/src/components/GameResultItem/GameResultItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
nickname,
nicknameContainer,
rankInfoContainer,
rankItem,
rankNumber,
rankPercent,
} from './GameResultItem.styled';

import useCountAnimation from '@/hooks/useCountAnimation';
import { GameFinalResult } from '@/types/balanceContent';

interface GameResultItemProps {
gameFinalResult: GameFinalResult;
}
const GameResultItem = ({ gameFinalResult }: GameResultItemProps) => {
const animatedRankPercent = useCountAnimation({
target: gameFinalResult.percent,
duration: 3000,
});

return (
<div css={rankItem}>
Copy link
Contributor

Choose a reason for hiding this comment

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

L4 - 변경 제안

상위 태그가 ol로 변경된다면 여기는 li 태그로 리스트임을 알려줄 수 있을 것 같아요 !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋습니당

<div css={rankInfoContainer}>
<span css={rankNumber}>{gameFinalResult.rank}</span>
<div css={nicknameContainer(animatedRankPercent)}>
<span>🥜</span>
<span css={nickname}>{gameFinalResult.name}</span>
</div>
</div>
<span css={rankPercent}>{animatedRankPercent}%</span>
</div>
);
};

export default GameResultItem;
15 changes: 15 additions & 0 deletions frontend/src/components/RoundResultTab/RoundResultTab.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { css } from '@emotion/react';

import { Theme } from '@/styles/Theme';

export const tabButtonStyle = (isActive: boolean) => css`
flex: 1;
padding: 0.8rem;
border-radius: 1.2rem 1.2rem 0 0;

background-color: ${isActive ? Theme.color.peanut400 : Theme.color.gray};

color: black;
font-weight: bold;
transition: all 0.5s;
`;
23 changes: 23 additions & 0 deletions frontend/src/components/RoundResultTab/RoundResultTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { tabButtonStyle } from './RoundResultTab.styled';

type TabTitle = 'group' | 'total';

interface RoundResultTabProps {
tab: TabTitle;
activeTab: TabTitle;
handleClickTab: (tab: TabTitle) => void;
}

const TAB_TITLE = {
group: '그룹',
total: '전체',
} as const;

const RoundResultTab = ({ tab, activeTab, handleClickTab }: RoundResultTabProps) => {
return (
<button css={tabButtonStyle(activeTab === tab)} onClick={() => handleClickTab(tab)}>
{TAB_TITLE[tab]}
</button>
);
};
export default RoundResultTab;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import useCountAnimation from '@/hooks/useCountAnimation';
import { Total, Group } from '@/types/roundVoteResult';

export const useTotalCountAnimation = (groupRoundResult?: Group, totalResult?: Total) => {
const animatedFirstPercent = useCountAnimation({ target: groupRoundResult?.firstOption.percent });
const animatedSecondPercent = useCountAnimation({
target: groupRoundResult?.secondOption.percent,
});

const animatedTotalFirstPercent = useCountAnimation({ target: totalResult?.firstOption.percent });
const animatedTotalSecondPercent = useCountAnimation({
target: totalResult?.secondOption.percent,
});

return {
animatedFirstPercent,
animatedSecondPercent,
animatedTotalFirstPercent,
animatedTotalSecondPercent,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { css } from '@emotion/react';

export const tabLayout = css`
display: flex;
flex-basis: 45%;
flex-direction: column;
width: 100%;

transition: all 1s;
`;

export const tabWrapperStyle = css`
display: flex;
width: 40%;
margin-left: 2.4rem;
`;
46 changes: 46 additions & 0 deletions frontend/src/components/RoundVoteContainer/RoundVoteContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from 'react';

import { useTotalCountAnimation } from './RoundVoteContainer.hook';
import { tabLayout, tabWrapperStyle } from './RoundVoteContainer.styled';
import RoundResultTab from '../RoundResultTab/RoundResultTab';
import TabContentContainer from '../TabContentContainer/TabContentContainer';

import useRoundVoteResultQuery from '@/hooks/useRoundVoteResultQuery';

const RoundVoteContainer = () => {
const [activeTab, setActiveTab] = useState<'group' | 'total'>('group');
const isGroupTabActive = activeTab === 'group';

const { groupRoundResult, totalResult } = useRoundVoteResultQuery();
const {
animatedFirstPercent,
animatedSecondPercent,
animatedTotalFirstPercent,
animatedTotalSecondPercent,
} = useTotalCountAnimation(groupRoundResult, totalResult);

const handleClickTab = (clickedTab: 'group' | 'total') => {
setActiveTab(clickedTab);
};

if (!groupRoundResult || !totalResult) return <div>데이터가 없습니다</div>;

return (
<div css={tabLayout}>
<div css={tabWrapperStyle}>
Copy link
Contributor

Choose a reason for hiding this comment

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

L4 - 변경 제안

tab이라서 div 대신에 nav 태그면 어떨까요 ?! 🙌

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오호 nav 태그는 페이지에서 다른 페이지로 이동하는 navbar에만 사용하는 줄 알았는데, 탭을 이동하여 다른 데이터를 보여준다는 점에서 nav 태그도 좋을 것 같아요!

<RoundResultTab tab="group" activeTab={activeTab} handleClickTab={handleClickTab} />
<RoundResultTab tab="total" activeTab={activeTab} handleClickTab={handleClickTab} />
</div>
<TabContentContainer
isGroupTabActive={isGroupTabActive}
roundResult={isGroupTabActive ? groupRoundResult : totalResult}
animatedFirstPercent={isGroupTabActive ? animatedFirstPercent : animatedTotalFirstPercent}
animatedSecondPercent={
isGroupTabActive ? animatedSecondPercent : animatedTotalSecondPercent
}
/>
</div>
);
};

export default RoundVoteContainer;
Loading