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

[REFACTOR] Context API를 활용하여 Modal 사용성 개선 #277

Merged
merged 22 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f23bc37
refactor: Modal을 context로 관리 #272
rbgksqkr Sep 20, 2024
db07003
test: Modal 리팩토링을 위한 게임 시작 테스트 코드 작성 #272
rbgksqkr Sep 20, 2024
016a4ea
refactor: 방장 여부 recoil 값을 넣어 렌더링하는 테스트 유틸 함수 공용화 #272
rbgksqkr Sep 20, 2024
0ec6545
refactor: customRenderWithIsMaster 테스트코드 적용 #272
rbgksqkr Sep 20, 2024
7aba249
refactor: 시작 버튼을 isMaster로 관리 #272
rbgksqkr Sep 20, 2024
86db138
refactor: Modal Context 게임 시작 부분 적용 #272
rbgksqkr Sep 20, 2024
8cf7398
refactor: Modal Context 투표 부분 적용 #272
rbgksqkr Sep 20, 2024
edb5f7d
refactor: Modal UI 역할이 사라져 StartButtonContainer 제거 #272
rbgksqkr Sep 20, 2024
f67f2f8
refactor: 게임 결과에도 Modal Context 적용 #272
rbgksqkr Sep 20, 2024
f620ab9
refactor: RoomSettingHeader에 Modal Context 적용 #272
rbgksqkr Sep 20, 2024
be3b4fd
refactor: 다른 모달도 적용할 수 있도록 Modal props 설정 #272
rbgksqkr Sep 20, 2024
45dd23b
fix: modal에서 toast를 사용하므로 toast를 modal 부모 요소로 수정 #272
rbgksqkr Sep 20, 2024
de1957d
refactor: 다음 라운드 안내 모달 Modal Context 적용 #272
rbgksqkr Sep 20, 2024
e435ed1
refactor: 초대하기 모달 Modal Context 적용 #272
rbgksqkr Sep 20, 2024
3de4785
refactor: 방 생성 및 참가 에러 모달 Modal Context 적용 #272
rbgksqkr Sep 20, 2024
cc96e08
fix: onConfirm 함수가 동작하지 않는 문제 해결 #272
rbgksqkr Sep 20, 2024
618f45f
refactor: 중복된 모달 하나로 합치기 #272
rbgksqkr Sep 20, 2024
34b3751
fix: Modal storybook 에 Provider 추가 #272
rbgksqkr Sep 20, 2024
9ba4988
fix: Modal에서 navigate 사용하지 못하는 오류 해결 #272
rbgksqkr Sep 23, 2024
9688a5e
fix: 브라우저 환경과 Provider 구조가 다른 문제 해결 #272
rbgksqkr Sep 23, 2024
ba0f7d8
merge: conflict 해결 #272
rbgksqkr Sep 26, 2024
4ad709d
merge: conflict 해결 #272
rbgksqkr Sep 26, 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
5 changes: 4 additions & 1 deletion frontend/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Theme } from '../src/styles/Theme';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../src/mocks/handlers';
import ToastProvider from '../src/providers/ToastProvider/ToastProvider';
import ModalProvider from '../src/providers/ModalProvider/ModalProvider';

initialize({
serviceWorker: {
Expand Down Expand Up @@ -39,7 +40,9 @@ const preview: Preview = {
<MemoryRouter initialEntries={['/']}>
<Global styles={GlobalStyle} />
<ToastProvider>
<Story />
<ModalProvider>
<Story />
</ModalProvider>
</ToastProvider>
</MemoryRouter>
</ThemeProvider>
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/App.tsx

This file was deleted.

12 changes: 9 additions & 3 deletions frontend/src/components/GameResult/GameResult.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';

import AlertModal from '../common/AlertModal/AlertModal';

import { fetchMatchingResult } from '@/apis/balanceContent';
import { resetRoom } from '@/apis/room';
import { QUERY_KEYS } from '@/constants/queryKeys';
import useModal from '@/hooks/useModal';
import { memberInfoState } from '@/recoil/atom';
import { MatchingResult, MemberMatchingInfo } from '@/types/balanceContent';
import { CustomError } from '@/utils/error';

type MatchingResultQueryResponse = UseQueryResult<MatchingResult, Error> & {
matchedMembers?: MemberMatchingInfo[];
Expand Down Expand Up @@ -34,11 +38,13 @@ export const useMatchingResultQuery = (): MatchingResultQueryResponse => {
};
};

export const useResetRoomMutation = (roomId: number, showModal: () => void) => {
export const useResetRoomMutation = (roomId: number) => {
const { show: showModal } = useModal();

return useMutation({
mutationFn: async () => await resetRoom(roomId),
onError: () => {
showModal();
onError: (error: CustomError) => {
showModal(AlertModal, { title: '방 초기화 에러', message: error.message });
Comment on lines +46 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

🌸 칭찬 🌸

이번 식으로 구현이 되는 군요. 전역 변수에 컴포넌트 자체를 담는 건 처음보네요

},
});
};
11 changes: 1 addition & 10 deletions frontend/src/components/GameResult/GameResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ import {
noMatchingImg,
noMatchingText,
} from './GameResult.styled';
import AlertModal from '../common/AlertModal/AlertModal';
import FinalButton from '../common/FinalButton/FinalButton';
import Spinner from '../common/Spinner/Spinner';
import GameResultItem from '../GameResultItem/GameResultItem';

import SadDdangKong from '@/assets/images/sadDdangkong.png';
import useModal from '@/hooks/useModal';

const GameResult = () => {
const { isOpen, show, close } = useModal();
const { matchedMembers, existMatching, isLoading } = useMatchingResultQuery();

return (
Expand Down Expand Up @@ -47,13 +44,7 @@ const GameResult = () => {
</div>
)}
</div>
<FinalButton showModal={show} />
<AlertModal
isOpen={isOpen}
onClose={close}
title="방 초기화 에러"
message="방을 초기화하는데 실패했어요. 다시 시도해주세요"
/>
<FinalButton />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

import ReadyMembersContainer from './ReadyMembersContainer';

import { customRender } from '@/utils/test-utils';

describe('ReadyMembersContainer 테스트', () => {
it('초대하기 버튼을 클릭했을 때, 초대 모달이 뜬다.', async () => {
const user = userEvent.setup();
customRender(<ReadyMembersContainer />);

const inviteButton = await screen.findByText('초대하기');
await user.click(inviteButton);

await waitFor(() => {
const copyText = screen.getByText('초대 링크 복사');
expect(copyText).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ import { memberInfoState } from '@/recoil/atom';

const ReadyMembersContainer = () => {
const { members, master } = useGetRoomInfo();
const { isOpen, show, close } = useModal();
const { show } = useModal();
const [memberInfo, setMemberInfo] = useRecoilState(memberInfoState);

const handleClickInvite = () => {
show(InviteModal);
};

// 원래 방장이 아니다 + 방장의 memberId와 내 memberId가 같다 -> 방장으로 변경
useEffect(() => {
if (!memberInfo.isMaster && master.memberId === memberInfo.memberId) {
Expand All @@ -39,7 +43,7 @@ const ReadyMembersContainer = () => {
<section css={membersContainer}>
<ul css={memberList}>
<li>
<button css={inviteButton} onClick={show}>
<button css={inviteButton} onClick={handleClickInvite}>
<div css={profileBox}>
<img src={plusIcon} alt="추가 아이콘" />
</div>
Expand All @@ -59,7 +63,6 @@ const ReadyMembersContainer = () => {
))}
</ul>
</section>
<InviteModal isOpen={isOpen} onClose={close} />
</section>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw';

import SelectContainer from './SelectContainer';

import { ERROR_MESSAGE } from '@/constants/message';
import { MOCK_API_URL } from '@/constants/url';
import BALANCE_CONTENT from '@/mocks/data/balanceContent.json';
import { server } from '@/mocks/server';
Expand All @@ -15,7 +16,10 @@ describe('SelectContainer', () => {
const user = userEvent.setup();
server.use(
http.post(MOCK_API_URL.vote, () => {
return new HttpResponse(JSON.stringify({ errorCode: '', message: '' }), { status: 400 });
return new HttpResponse(
JSON.stringify({ errorCode: 'ALREADY_VOTED', message: ERROR_MESSAGE.ALREADY_VOTED }),
{ status: 400 },
);
}),
);

Expand Down
11 changes: 0 additions & 11 deletions frontend/src/components/SelectContainer/SelectContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@ import { useParams } from 'react-router-dom';
import useSelectOption from './hooks/useSelectOption';
import { selectContainerLayout, selectSection } from './SelectContainer.styled';
import Timer from './Timer/Timer';
import AlertModal from '../common/AlertModal/AlertModal';
import SelectButton from '../common/SelectButton/SelectButton';

import SelectOption from '@/components/SelectOption/SelectOption';
import useBalanceContentQuery from '@/hooks/useBalanceContentQuery';
import useModal from '@/hooks/useModal';

const SelectContainer = () => {
const { roomId } = useParams();
const { balanceContent } = useBalanceContentQuery(Number(roomId));
const { selectedOption, handleClickOption, completeSelection } = useSelectOption();
const { isOpen, show, close } = useModal();

return (
<div css={selectContainerLayout}>
<Timer
selectedId={selectedOption.id}
isVoted={selectedOption.isCompleted}
completeSelection={completeSelection}
showModal={show}
/>
<section css={selectSection}>
<SelectOption
Expand All @@ -41,13 +37,6 @@ const SelectContainer = () => {
contentId={balanceContent.contentId}
selectedId={selectedOption.id}
completeSelection={completeSelection}
showModal={show}
/>
<AlertModal
isOpen={isOpen}
onClose={close}
title="선택 에러"
message={'선택이 정상적으로 반영되지 않았어요.'}
/>
</div>
);
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/SelectContainer/Timer/Timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,16 @@ interface TimerProps {
selectedId: number;
isVoted: boolean;
completeSelection: () => void;
showModal: () => void;
}

const Timer = ({ selectedId, isVoted, completeSelection, showModal }: TimerProps) => {
const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => {
const { roomId } = useParams();
const { balanceContent, isFetching } = useBalanceContentQuery(Number(roomId));
const { barWidthPercent, leftRoundTime, isAlmostFinished } = useVoteTimer({
roomId: Number(roomId),
selectedId,
isVoted,
completeSelection,
showModal,
});

useVoteIsFinished({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,16 @@ interface UseVoteTimerProps {
selectedId: number;
isVoted: boolean;
completeSelection: () => void;
showModal: () => void;
}

const useVoteTimer = ({
roomId,
selectedId,
isVoted,
completeSelection,
showModal,
}: UseVoteTimerProps) => {
const useVoteTimer = ({ roomId, selectedId, isVoted, completeSelection }: UseVoteTimerProps) => {
const { balanceContent } = useBalanceContentQuery(roomId);
const timeLimit = balanceContent.timeLimit || DEFAULT_TIME_LIMIT_MSEC;

const { mutate: vote } = useCompleteSelectionMutation({
selectedId,
contentId: balanceContent.contentId,
completeSelection,
showModal,
});

const { leftRoundTime, barWidthPercent, isAlmostFinished } = useTimer({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';

import StartButton from './StartButton';

import { ERROR_MESSAGE } from '@/constants/message';
import { MOCK_API_URL } from '@/constants/url';
import { server } from '@/mocks/server';
import { customRenderWithIsMaster } from '@/utils/test-utils';

describe('StartButton 테스트', () => {
it('시작 버튼을 클릭했을 때, 게임 시작 API에서 에러가 발생하면 알림 모달이 뜬다.', async () => {
const user = userEvent.setup();
server.use(
http.patch(MOCK_API_URL.startGame, async () => {
return HttpResponse.json(
{
errorCode: 'NOT_READY_ROOM',
message: ERROR_MESSAGE.NOT_READY_ROOM,
},
{ status: 400 },
);
}),
);

customRenderWithIsMaster(<StartButton />, true);

const startButton = await screen.findByRole('button', { name: '시작' });
await user.click(startButton);

await waitFor(() => {
const closeIcon = screen.getByAltText('닫기 버튼');
expect(closeIcon).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { useGameStart } from './hooks/useGameStart';

import Button from '@/components/common/Button/Button';

interface StartButtonProps {
show: () => void;
}

const StartButton = ({ show }: StartButtonProps) => {
const { memberInfo, handleGameStart } = useGameStart({ showModal: show });
const StartButton = () => {
const { memberInfo, handleGameStart } = useGameStart();

return (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@ import { useParams } from 'react-router-dom';
import { useRecoilState } from 'recoil';

import { startGame } from '@/apis/room';
import AlertModal from '@/components/common/AlertModal/AlertModal';
import useModal from '@/hooks/useModal';
import useToast from '@/hooks/useToast';
import { memberInfoState } from '@/recoil/atom';
import { CustomError, NetworkError } from '@/utils/error';

const isServerError = (status: number) => status >= 500 && status !== 555;

interface UseGameStartProps {
showModal: () => void;
}

export const useGameStart = ({ showModal }: UseGameStartProps) => {
export const useGameStart = () => {
const [memberInfo, setMemberInfo] = useRecoilState(memberInfoState);
const { roomId } = useParams();
const { show } = useToast();
const { show: showModal } = useModal();

const startGameMutation = useMutation({
mutationFn: () => startGame(Number(roomId)),
Expand All @@ -26,7 +25,7 @@ export const useGameStart = ({ showModal }: UseGameStartProps) => {
return;
}

showModal();
showModal(AlertModal, { title: '게임 시작 에러', message: error.message });
},
networkMode: 'always',
throwOnError: (error: CustomError) => isServerError(error.status),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import Countdown from './Countdown/Countdown';
import useCountdown from './hooks/useCountdown';
import StartButton from './StartButton/StartButton';
import AlertModal from '../common/AlertModal/AlertModal';
import useCountdown from '../hooks/useCountdown';

import { useGetRoomInfo } from '@/hooks/useGetRoomInfo';
import useModal from '@/hooks/useModal';

const StartButtonContainer = () => {
const { isOpen, show, close } = useModal();
const { isGameStart } = useGetRoomInfo();
const { isCountdownStart, goToGame } = useCountdown({ isGameStart });

return (
<>
{isCountdownStart && <Countdown goToGame={goToGame} />}
<StartButton show={show} />
<AlertModal
isOpen={isOpen}
onClose={close}
title="게임 시작 에러"
message="게임을 시작할 수 없습니다."
/>
<StartButton />
</>
);
};
Expand Down
Loading
Loading