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] 선착순 예외 처리 및 UX 개선 #190

Merged
merged 37 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5afd2be
chore: test API 삭제
sooyeoniya Aug 19, 2024
bdf98fc
feat: 선착순 게임 새로고침 및 뒤로가기 방지
sooyeoniya Aug 19, 2024
47d5f69
feat: FinalResult 화면에서 unblockNavigation 호출
sooyeoniya Aug 19, 2024
974d8f2
feat: 사용자가 이미 옵션 선택한 후에는 새로고침/뒤로가기 방지 풀기
sooyeoniya Aug 20, 2024
1c0df68
chore: 현재 시간을 서버 시간으로 변경
sooyeoniya Aug 20, 2024
9ab86d2
feat: 0%일 때 프로그래스바 처리
sooyeoniya Aug 20, 2024
36365dc
feat: 유저 응모 여부에 따른 FinalResult 처리
sooyeoniya Aug 20, 2024
107a778
fix: result API 데이터 반환 타입 변경
sooyeoniya Aug 20, 2024
c59978e
chore: 필요 없는 TODO 삭제
sooyeoniya Aug 20, 2024
07ce1ba
feat: resultAPI의 optionId 추가 후 사용자가 선택한 optionId 반영
sooyeoniya Aug 20, 2024
cd41ede
feat: 게임 진입 시 현재 게임 진행 상태 및 사용자 참여 여부 저장 로직 구현 및 카운트 다운 로직 분리
sooyeoniya Aug 20, 2024
2a1da5a
feat: 오늘 날짜 필터링 추가
sooyeoniya Aug 21, 2024
276b538
refactor: Context 내부 getRushUserParticipationStatus API 호출 밖으로 분리
sooyeoniya Aug 21, 2024
4c8f181
feat: 게임 재접속 시 유저 상태에 따른 상태 처리
sooyeoniya Aug 21, 2024
cd478e7
rename: updateCardOptions 함수명 변경
sooyeoniya Aug 21, 2024
c88e43d
refactor: getSelectedCardInfo 유틸 함수로 분리
sooyeoniya Aug 21, 2024
47ab45b
refactor: getOptionRatio 유틸 함수로 분리
sooyeoniya Aug 21, 2024
dff3181
fix: useFetch 콜백 쌓이는 오류 수정
sooyeoniya Aug 21, 2024
9312f4b
refactor: rushBalanceData 패칭 및 상태 업데이트하는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
b5e604c
refactor: todayRushEventData 패칭 및 상태 업데이트하는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
270c911
refactor: rushUserParticipationStatus 패칭 및 상태 업데이트하는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
ab56abd
refactor: resultData 패칭 및 상태 업데이트하는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
1120dee
refactor: userResultData 패칭 및 상태 업데이트하는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
b24076e
refactor: rushData 받아서 게임 초기 상태 설정해주는 로직 훅으로 분리
sooyeoniya Aug 21, 2024
9b1b8db
refactor: 이전 커밋에 대한 useSetGamePhase 훅 추가 커밋
sooyeoniya Aug 21, 2024
7ddf770
refactor: context에 flux 패턴 적용
sooyeoniya Aug 21, 2024
47b4249
refactor: hooks 폴더 리팩토링 및 RushGame enum 변환
sooyeoniya Aug 22, 2024
a9aad6f
refactor: 이벤트 공유 컴포넌트 분리 및 FinalResult UX 개선
sooyeoniya Aug 22, 2024
5c02ed3
refactor: Countdown UX 개선
sooyeoniya Aug 22, 2024
bd01cee
refactor: CountdownTimer 인터페이스 분리
sooyeoniya Aug 22, 2024
48aa9af
feat: 토글 버튼 클릭 시 실시간 비율 반영 추가
sooyeoniya Aug 22, 2024
21a5846
Merge branch 'dev' into feat/#170-rush-exception
sooyeoniya Aug 22, 2024
45985ff
refactor: 날짜와 시간 분리하는 로직 유틸로 빼기
sooyeoniya Aug 22, 2024
e1dc865
refactor: parseIsoDateTime 적용
sooyeoniya Aug 22, 2024
0a8f509
refactor: timeout if 조건문 상수로 분리
sooyeoniya Aug 22, 2024
895057f
refactor: Suspense 추가 및 관련 컴포넌트 리팩토링
sooyeoniya Aug 22, 2024
a4b6358
refactor: getOptionRatio, getSelectedCardInfo props 수정
sooyeoniya Aug 22, 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
9 changes: 9 additions & 0 deletions client/src/components/Suspense/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PropsWithChildren } from "react";

interface SuspenseProps extends PropsWithChildren {
isLoading?: boolean;
}

export default function Suspense({ children, isLoading = false }: SuspenseProps) {
return <>{isLoading ? <></> : children}</>;
}
275 changes: 60 additions & 215 deletions client/src/contexts/rushGameContext.tsx
Original file line number Diff line number Diff line change
@@ -1,226 +1,71 @@
import { ReactNode, createContext, useCallback, useEffect, useState } from "react";
import { useCookies } from "react-cookie";
import { useLoaderData } from "react-router-dom";
import { RushAPI } from "@/apis/rushAPI.ts";
import { CARD_COLOR, CARD_OPTION, CARD_PHASE } from "@/constants/Rush/rushCard";
import { COOKIE_KEY } from "@/constants/cookie.ts";
import useCountdown from "@/hooks/useCountdown.ts";
import useFetch from "@/hooks/useFetch.ts";
import { ReactNode, createContext, useReducer } from "react";
import { CARD_COLOR, CARD_OPTION } from "@/constants/Rush/rushCard";
import {
GetRushBalanceResponse,
GetRushUserParticipationStatusResponse,
GetTotalRushEventsResponse,
} from "@/types/rushApi.ts";
import { CardOption, CardOptionState, GamePhase, RushGameContextType } from "@/types/rushGame";
import { getMsTime } from "@/utils/getMsTime.ts";

export const RushGameContext = createContext<RushGameContextType | undefined>(undefined);

export const RushGameProvider = ({ children }: { children: ReactNode }) => {
// const navigate = useNavigate();
const [cookies] = useCookies([COOKIE_KEY.ACCESS_TOKEN]);
const rushData = useLoaderData() as GetTotalRushEventsResponse;
const [initialPreCountdown, setInitialPreCountdown] = useState<number | null>(null);
const [initialRunCountdown, setInitialRunCountdown] = useState<number | null>(null);

const [gameState, setGameState] = useState<RushGameContextType["gameState"]>({
phase: CARD_PHASE.NOT_STARTED,
userParticipatedStatus: false,
userSelectedOption: CARD_OPTION.LEFT_OPTIONS,
cardOptions: {
[CARD_OPTION.LEFT_OPTIONS]: {
mainText: "",
subText: "",
resultMainText: "",
resultSubText: "",
color: CARD_COLOR.GREEN,
selectionCount: 0,
},
[CARD_OPTION.RIGHT_OPTIONS]: {
mainText: "",
subText: "",
resultMainText: "",
resultSubText: "",
color: CARD_COLOR.BLUE,
selectionCount: 0,
},
},
});

const setGamePhase = useCallback((phase: GamePhase) => {
setGameState((prevState) => ({ ...prevState, phase }));
}, []);

const setUserParticipationStatus = useCallback((status: boolean) => {
setGameState((prevState) => ({ ...prevState, userParticipatedStatus: status }));
}, []);

const setUserSelectedOption = useCallback((option: CardOption) => {
setGameState((prevState) => ({ ...prevState, userSelectedOption: option }));
}, []);

const updateCardOptions = useCallback(
(option: CardOption, updates: Partial<CardOptionState>) => {
setGameState((prevState) => ({
...prevState,
cardOptions: {
...prevState.cardOptions,
[option]: { ...prevState.cardOptions[option], ...updates },
},
}));
RUSH_ACTION,
RushGameAction,
RushGameDispatchType,
RushGameStateType,
} from "@/types/rushGame";

export const RushGameStateContext = createContext<RushGameStateType | undefined>(undefined);
export const RushGameDispatchContext = createContext<RushGameDispatchType | undefined>(undefined);

const initialGameState: RushGameStateType = {
phase: null,
userParticipatedStatus: false,
userSelectedOption: CARD_OPTION.LEFT_OPTIONS,
cardOptions: {
[CARD_OPTION.LEFT_OPTIONS]: {
mainText: "",
subText: "",
resultMainText: "",
resultSubText: "",
color: CARD_COLOR.GREEN,
selectionCount: 0,
},
[]
);

const {
data: userParticipatedStatus,
isSuccess: isSuccessUserParticipationStatus,
fetchData: getRushUserParticipationStatus,
} = useFetch<GetRushUserParticipationStatusResponse, string>((token) =>
RushAPI.getRushUserParticipationStatus(token)
);

const updateUserStatusAndSelectedOption = useCallback(
async (token: string, selectedOption: CardOption) => {
await getRushUserParticipationStatus(token);
setUserSelectedOption(selectedOption);
[CARD_OPTION.RIGHT_OPTIONS]: {
mainText: "",
subText: "",
resultMainText: "",
resultSubText: "",
color: CARD_COLOR.BLUE,
selectionCount: 0,
},
[]
);

useEffect(() => {
if (isSuccessUserParticipationStatus && userParticipatedStatus) {
setUserParticipationStatus(userParticipatedStatus);
}
}, [isSuccessUserParticipationStatus, userParticipatedStatus]);
},
};

const getSelectedCardInfo = useCallback(
(option: CardOption) => {
const cardInfo = gameState.cardOptions[option];
const rushGameReducer = (state: RushGameStateType, action: RushGameAction): RushGameStateType => {
switch (action.type) {
case RUSH_ACTION.SET_PHASE:
return { ...state, phase: action.payload };
case RUSH_ACTION.SET_USER_PARTICIPATION:
return { ...state, userParticipatedStatus: action.payload };
case RUSH_ACTION.SET_USER_OPTION:
return { ...state, userSelectedOption: action.payload };
case RUSH_ACTION.SET_CARD_OPTIONS:
return {
mainText: cardInfo.mainText,
subText: cardInfo.subText,
resultMainText: cardInfo.resultMainText,
resultSubText: cardInfo.resultSubText,
color: cardInfo.color,
selectionCount: cardInfo.selectionCount,
...state,
cardOptions: {
...state.cardOptions,
[action.payload.option]: {
...state.cardOptions[action.payload.option],
...action.payload.updates,
},
},
};
},
[gameState.userSelectedOption, gameState.cardOptions]
);

const {
data: rushBalanceData,
isSuccess: isSuccessRushBalance,
fetchData: getRushBalance,
} = useFetch<GetRushBalanceResponse, string>((token) => RushAPI.getRushBalance(token));

const fetchRushBalance = useCallback(async (): Promise<void> => {
await getRushBalance(cookies[COOKIE_KEY.ACCESS_TOKEN]);
}, [cookies, getRushBalance]);

useEffect(() => {
if (isSuccessRushBalance && rushBalanceData) {
const { leftOption, rightOption } = rushBalanceData;

updateCardOptions(CARD_OPTION.LEFT_OPTIONS, {
selectionCount: leftOption,
});
updateCardOptions(CARD_OPTION.RIGHT_OPTIONS, {
selectionCount: rightOption,
});
}
}, [isSuccessRushBalance, rushBalanceData]);

const getOptionRatio = useCallback(
(option: CardOption): number => {
const total =
gameState.cardOptions[CARD_OPTION.LEFT_OPTIONS].selectionCount +
gameState.cardOptions[CARD_OPTION.RIGHT_OPTIONS].selectionCount;
if (total === 0) return 0;
const ratio = (gameState.cardOptions[option].selectionCount / total) * 100;
return Math.round(ratio * 100) / 100;
},
[gameState.cardOptions]
);

useEffect(() => {
const currentEvent = rushData.events.find(
(event) => event.rushEventId === rushData.todayEventId
);
if (currentEvent) {
const serverTime = getMsTime(rushData.serverTime);
const startTime = getMsTime(currentEvent.startDateTime);
const endTime = getMsTime(currentEvent.endDateTime);

if (
gameState.phase === CARD_PHASE.NOT_STARTED &&
rushData.serverTime &&
currentEvent?.startDateTime
) {
const preCountdown = Math.max(0, Math.floor((startTime - serverTime) / 1000));
setInitialPreCountdown(preCountdown);
} else if (
gameState.phase === CARD_PHASE.IN_PROGRESS &&
rushData.serverTime &&
currentEvent?.endDateTime
) {
const runCountdown = Math.max(0, Math.floor((endTime - serverTime) / 1000));
setInitialRunCountdown(runCountdown);
}
}
}, [rushData, gameState.phase]);

const preCountdown = useCountdown(initialPreCountdown || 1);
const runCountdown = useCountdown(initialRunCountdown || 1);

// TEST COUNTDOWN CODE
// const preCountdown = useCountdown(5);
// const runCountdown = useCountdown(20);

useEffect(() => {
if (preCountdown <= 0 && gameState.phase === CARD_PHASE.NOT_STARTED) {
setGamePhase(CARD_PHASE.IN_PROGRESS);
} else if (runCountdown <= 0 && gameState.phase === CARD_PHASE.IN_PROGRESS) {
setGamePhase(CARD_PHASE.COMPLETED);
}
}, [preCountdown, runCountdown]);
default:
return state;
}
};

// useEffect(() => {
// const currentEvent = rushData.events.find(
// (event) => event.rushEventId === rushData.todayEventId
// );
//
// if (currentEvent && gameState.phase === CARD_PHASE.COMPLETED) {
// const serverTime = getMsTime(rushData.serverTime);
// const endTime = getMsTime(currentEvent.endDateTime);
//
// if (!gameState.userParticipatedStatus) {
// if (serverTime > endTime) {
// navigate("/rush");
// // TODO: 이벤트 참여기간 아닌 분기 처리(이벤트 종료 후 그 당일 12시 직전까지)
// }
// } else {
// // TODO: 참여한 사람일 경우 당일 12시 직전까지 계속 "COMPLETED" 상태 유지
// // TODO: 12시 지나면 다시 "NOT_STARTED" 상태로 변경
// }
// }
// }, [gameState.phase, gameState.userParticipatedStatus, rushData, navigate]);
export const RushGameProvider = ({ children }: { children: ReactNode }) => {
const [gameState, dispatch] = useReducer(rushGameReducer, initialGameState);

return (
<RushGameContext.Provider
value={{
gameState,
preCountdown,
runCountdown,
updateCardOptions,
updateUserStatusAndSelectedOption,
getSelectedCardInfo,
getOptionRatio,
fetchRushBalance,
}}
>
{children}
</RushGameContext.Provider>
<RushGameDispatchContext.Provider value={dispatch}>
<RushGameStateContext.Provider value={gameState}>
{children}
</RushGameStateContext.Provider>
</RushGameDispatchContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import { CasperCardBackUI } from "./CasperCardBackUI";

interface MyCasperCardBackProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo, useCallback } from "react";
import { CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomDispatchContext from "@/hooks/Contexts/useCasperCustomDispatchContext.ts";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import { CASPER_ACTION } from "@/types/casperCustom";
import { CasperCardFrontUI } from "./CasperCardFrontUI";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback, useEffect } from "react";
import { cva } from "class-variance-authority";
import { CASPER_OPTION, CUSTOM_OPTION, OPTION_TYPE } from "@/constants/CasperCustom/casper";
import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomDispatchContext from "@/hooks/Contexts/useCasperCustomDispatchContext.ts";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import { CASPER_ACTION } from "@/types/casperCustom";
import { CustomOptionImageItem } from "./CustomOptionImageItem";
import { EyesPanel as EyesPanelComponent } from "./EyesPanel";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
OPTION_TYPE,
POSITION_OPTION,
} from "@/constants/CasperCustom/casper";
import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomDispatchContext from "@/hooks/Contexts/useCasperCustomDispatchContext.ts";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import { CASPER_ACTION } from "@/types/casperCustom";
import { CasperCustomPanelLayout } from "./CasperCustomPanelLayout";
import { EyesOptionImageItem } from "./EyesOptionImageItem";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { DISSOLVE } from "@/constants/animation";
import { SCROLL_MOTION } from "@/constants/animation";
import { COOKIE_KEY } from "@/constants/cookie";
import { MyCasperCardFront } from "@/features/CasperCustom/CasperCard/MyCasperCardFront";
import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomDispatchContext from "@/hooks/Contexts/useCasperCustomDispatchContext.ts";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import useFetch from "@/hooks/useFetch";
import useToast from "@/hooks/useToast";
import { CASPER_ACTION } from "@/types/casperCustom";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { DISSOLVE } from "@/constants/animation";
import { SCROLL_MOTION } from "@/constants/animation";
import { CasperFlipCard } from "@/features/CasperCustom/CasperCard/CasperFlipCard";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import useToast from "@/hooks/useToast";
import type { CasperCardType } from "@/types/casper";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { DISSOLVE } from "@/constants/animation";
import { SCROLL_MOTION } from "@/constants/animation";
import { COOKIE_KEY } from "@/constants/cookie";
import { MyCasperCardFront } from "@/features/CasperCustom/CasperCard/MyCasperCardFront";
import useCasperCustomDispatchContext from "@/hooks/useCasperCustomDispatchContext";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomDispatchContext from "@/hooks/Contexts/useCasperCustomDispatchContext.ts";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import useFetch from "@/hooks/useFetch";
import { CASPER_ACTION } from "@/types/casperCustom";
import { CasperInformationType, PostCasperResponse } from "@/types/lotteryApi";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CUSTOM_OPTION_ARRAY } from "@/constants/CasperCustom/customStep";
import { DISSOLVE } from "@/constants/animation";
import { SCROLL_MOTION } from "@/constants/animation";
import { MyCasperCardFront } from "@/features/CasperCustom/CasperCard/MyCasperCardFront";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useCasperCustomStateContext from "@/hooks/Contexts/useCasperCustomStateContext.ts";
import { getCasperOptionDescription } from "@/utils/CasperCustom/getCasperOptionDescription";

interface CasperCustomProcessProps {
Expand Down
Loading
Loading