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] 선착순 밸런스 게임 구현 #158

Merged
merged 114 commits into from
Aug 18, 2024
Merged

[Feat] 선착순 밸런스 게임 구현 #158

merged 114 commits into from
Aug 18, 2024

Conversation

sooyeoniya
Copy link
Member

@sooyeoniya sooyeoniya commented Aug 15, 2024

🖥️ Preview

메인 페이지 API 연동

2024-08-15.6.30.15.mov

선착순 밸런스 게임 구현

2024-08-15.6.43.57.mov

✏️ 한 일

  • 메인 페이지 이벤트 경품 API 연동
  • 메인 페이지 이벤트 기간 API 연동
    • 전체, 선착순, 추첨 이벤트 기간
  • Notice 컴포넌트 이벤트 기간 API 연동
  • date util 전체 함수 구현
  • 선착순 밸런스 게임 구현
    • 게임 입장 전 팝업창 및 사용자 토큰 발급 연동
    • 밸런스 게임 카드 컴포넌트 구현
    • 전체 섹션 별 컴포넌트 구현
    • Rush API 연동
    • 밸런스 게임 전체 상태 관리
    • 밸런스 게임 전체 로직 구현
  • 기획 변경 사항
    • 밸런스 게임 진입 페이지 추가 구현 (채민님께서 배경 추가해주심!!🤩)
    • 밸런스 게임 카드 옵션 선택하는 페이지에 타이머 추가 (메인 텍스트 부분 5초 뒤 타이머로 바뀜)
    • 결과 페이지에서 동점인 경우 동점에 대한 결과 문구 삭제 및 선착순 당첨 여부에 따라 "축하해요", "아쉽네요" 문구 두 개로 픽스 ("동점이에요" 문구는 참여자가 당첨 여부를 확인할 수 없다고 판단하여 삭제했습니다.)
    • 결과 페이지 win 부분의 옵션 메인 텍스트 크기가 커지는 스타일 기각(옵션 메인 텍스트가 길어지면 사용자가 보기에 별로인 것 같다고 판단하여 팀원과 상의 후 해당 스타일은 반영하지 않는 것으로 결정했습니다.)
    • 결과 페이지에 "당신의 선택" 카테고리 추가(참여자가 어느 옵션을 선택했는지 확인할 수 없는 UX 문제 해결)
      close 전체 date util 함수 구현 #69
      close 선착순 밸런스 게임 콘텐츠 카드 컴포넌트 구현 #82
      close 선착순 이벤트 기능 구현 #156
      close 메인 페이지 API 연동 #157

❗️ 발생한 이슈 (해결 방안)

✏️ 선착순 밸런스 게임 상태 관리
나머지는 위키에 정리해서 올리겠습니다! 좀만 기다려주세용 😭ㅠㅠ
이슈 별로 브랜치 많이 못나눈 점에 대해 사과드립니다 ..🥺

❓ 논의가 필요한 사항

  • 선착순 밸런스 게임 진행 전과 진행 중에 대한 카운트 다운을 API로 시간을 연동하게 되면 밤 10시까지 게임 시작하기를 기다려야 하는데, 데모할 때 밤 10시까지 기다릴 수는 없다고 생각해서 임시로 시간 따로 설정해두었습니다. (preCountdown, runCountdown) 만약 발표 날에 시연하게 되면 선착순 밸런스 게임을 어떻게 실시간으로 보여줄 수 있을지 고민해보면 좋을 것 같습니다!

✔️ 앞으로 할 일

  • PR 충돌 해결하기
  • token.ts -> cookie.ts 타입 변경 반영하기
  • ErrorBoundary, useFetch, fetchWithTimeout, getMsTime 코드 반영하기
  • Merge 시 발생하는 오류 해결하기
  • 오늘 선착순 이벤트 종료 시 사용자 참여 여부에 따른 화면 분기 처리
  • 새로고침 방지 알림 붙이기
  • SelectedCard 에서 새로고침 버튼 클릭 시 실시간으로 비율이 반영 되는지 확인하기 (with 백엔드 배재인)
  • 새로고침 툴팁 메시지 부드럽게 사라지는 인터렉션 추가
  • 가끔 팝업 뜨기 전에 “이벤트 기간이 아닙니다”라는 토스트 메시지 뜨는 오류
  • RushCard 색상 랜덤 기능 구현
  • 선착순 밸런스 게임 불필요한 렌더링 줄이기 (카운트 다운 시 발생하는 불필요한 렌더링)
  • 선착순 밸런스 게임 렌더링 약간 느림 이슈 해결하기
  • 선착순 밸런스 게임 리팩토링
  • 선착순 밸런스 게임 이벤트 공유 링크 연동하기

@sooyeoniya sooyeoniya added the feat 기능 구현 label Aug 15, 2024
@sooyeoniya sooyeoniya requested a review from jhj2713 August 15, 2024 10:13
@sooyeoniya sooyeoniya self-assigned this Aug 15, 2024
Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 성공했습니다! 🎉

Copy link

빌드를 실패했습니다. ❌ 자세한 내용은 로그를 참고해주세요.

Copy link

빌드를 성공했습니다! 🎉

Copy link
Member

@jhj2713 jhj2713 left a comment

Choose a reason for hiding this comment

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

복잡한 스펙이었는데 구현하느라 고생 많았어유ㅠㅠ👍

Comment on lines 32 to 35
[key in (typeof CARD_DAYS)[keyof typeof CARD_DAYS]]: {
[CARD_OPTION.LEFT_OPTIONS]: CardColor;
[CARD_OPTION.RIGHT_OPTIONS]: CardColor;
};
Copy link
Member

Choose a reason for hiding this comment

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

요것도 따로 interface로 빼줘도 좋을 것 같아용 CardColorType 같은 식으로요!

Copy link
Member Author

Choose a reason for hiding this comment

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

반영했습니다 🤩

Comment on lines +195 to +202
gameState,
preCountdown,
runCountdown,
updateCardOptions,
updateUserStatusAndSelectedOption,
getSelectedCardInfo,
getOptionRatio,
fetchRushBalance,
Copy link
Member

Choose a reason for hiding this comment

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

너무 많은 setter가 내려가는 것 같아서 요거 나중에 flux 패턴 적용해서 action dispatch 하게 해주면 더 깔끔해지고 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

알겠습니다! 조만간 한 번 적용해서 리팩토링 해볼게여!!

Comment on lines 136 to 152
switch (gameState.phase) {
case CARD_PHASE.NOT_STARTED:
if (rushData.serverTime && currentEvent?.startDateTime) {
const preCountdown = Math.max(
0,
Math.floor((startTime - serverTime) / 1000)
);
setInitialPreCountdown(preCountdown);
}
break;
case CARD_PHASE.IN_PROGRESS:
if (rushData.serverTime && currentEvent?.endDateTime) {
const runCountdown = Math.max(0, Math.floor((endTime - serverTime) / 1000));
setInitialRunCountdown(runCountdown);
}
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
switch (gameState.phase) {
case CARD_PHASE.NOT_STARTED:
if (rushData.serverTime && currentEvent?.startDateTime) {
const preCountdown = Math.max(
0,
Math.floor((startTime - serverTime) / 1000)
);
setInitialPreCountdown(preCountdown);
}
break;
case CARD_PHASE.IN_PROGRESS:
if (rushData.serverTime && currentEvent?.endDateTime) {
const runCountdown = Math.max(0, Math.floor((endTime - serverTime) / 1000));
setInitialRunCountdown(runCountdown);
}
break;
}
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);
}

switch랑 if문 같이 쓰는 것보다 이렇게 조건 묶어주는게 더 간단하지 않을까욥?

);
}

function OptionDisplay({
Copy link
Member

Choose a reason for hiding this comment

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

OptionDisplay는 컴포넌트 파일로 따로 빼도 좋지 않을까용? 필요한 데이터는 다 props로 받고 있어서 이 파일에 있을 이유가 딱히 없어보여서요!

Copy link
Member Author

Choose a reason for hiding this comment

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

FinalResult 컴포넌트에도 유사한 OptionDisplay가 있어서 둘 다 다른 이름의 컴포넌트로 분리했습니다!! (RushCurrentOptionDisplay, RushResultOptionDisplay)

Comment on lines 47 to 53
function ReloadButton({ onClick }: { onClick: () => void }) {
return (
<button onClick={onClick}>
<Reload />
</button>
);
}
Copy link
Member

Choose a reason for hiding this comment

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

요건 진짜 개인적인 의견인데 이정도는 공통화까지는 안 해도 되지 않을까 싶어욥,,! button 태그 하나 감싸주는건데 컴포넌트로 빼게 되면 오히려 컴포넌트 선언문이 길어지게 돼서용

LOSING: "아쉽네요, 다음 기회를 다시 노려봐요.",
};

type WinStatus = "Win" | "Lose" | "Tie";
Copy link
Member

Choose a reason for hiding this comment

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

win, lose, tie를 상수로 빼서 사용하면 어떨까용


const fetchRushBalance = useCallback(async (): Promise<void> => {
try {
const rushBalanceData = await RushAPI.getRushBalance(cookies[COOKIE_TOKEN_KEY]);
Copy link
Member

Choose a reason for hiding this comment

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

아 그리구 fetch 요청하는 부분은 사용하는 컴포넌트로 빼고 context 에서는 setting하는 dispatch 함수만 빼서 값을 넣어주는 용도로만 사용하는게 어떨까용? 지금 provider가 값을 가공해서 넣어주는 용도 외에 데이터 패칭으로도 쓰이니까 너무 많은 일을 한다는 생각이 들어서요!

Copy link
Member

Choose a reason for hiding this comment

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

이건 진짜 개인적인 생각이에유,,, 여러번 사용되는 코드라 재사용을 위해서 context에 선언하는게 나은거면 이렇게 해도 좋은 것 같다구 생각합니당

Copy link
Member Author

Choose a reason for hiding this comment

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

제가 RushGame 관련 컴포넌트 중에 두 개 이상에서 동일한 호출이 발생하는 경우에 공통으로 분리해주기 위해서 context로 따로 추출한건데, 앞서 말씀해주신 flux 패턴 관련해서 적용할 때 따로 뺄지 말지 한 번 더 고민해볼게용! 저도 사실 구현하면서 뭔가 provider 코드가 너무 길어진다는 느낌이 들어서 리팩토링할 필요가 있다고 느끼긴 했어요ㅠㅠ 좋은 의견 감사합니다 🤩

Comment on lines 33 to 41
useEffect(() => {
if (inviteUser) {
setCookie(COOKIE_KEY.INVITE_USER, inviteUser);
}
}, [inviteUser]);

useEffect(() => {
if (authToken && isSuccessGetAuthToken) {
setCookie(COOKIE_KEY.ACCESS_TOKEN, authToken.accessToken);
Copy link
Member

Choose a reason for hiding this comment

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

setCookie 할 때 path 설정 해줘야하지 않았나용,,? 저번에 그래야 path에 구애받지 않고 cookie를 쓸 수 있었던 것 같아서용

Copy link
Member Author

@sooyeoniya sooyeoniya Aug 16, 2024

Choose a reason for hiding this comment

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

우선 루트로 잘 만들어지긴 하는데 setCookie부분 다 path 명시해둘게용

Comment on lines 21 to 24
// DATA RESET TEST API
const { fetchData: getRushTodayEventTest } = useFetch<RushEventStatusCodeResponse, string>(
(token) => RushAPI.getRushTodayEventTest(token)
);
Copy link
Member

Choose a reason for hiding this comment

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

요건 나중에 삭제 하는거죵?

Copy link
Member Author

Choose a reason for hiding this comment

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

네 맞습니당~ 나중에는 그냥 백엔드에서 직접 해준다고 우선 제 쪽에서 테스트용으로 쓰라고 준거에용

Copy link
Member Author

Choose a reason for hiding this comment

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

갑자기 또 이 테스트 API만 아래 400 Bad Request 오류 뜨네요,,
{errorCode: "JWT_PARSE_EXCEPTION", message: "Json 파싱 오류입니다."}
뭔가 테스트 API가 종종 이런 오류가 있는 것 같은데, 실질적으로 최종 배포될 때는 없애는게 맞고,
지금 dev에서는 그냥 주석 처리하고 개발하면서 데이터 리셋할 때 주석 해제하고 사용해도 될 것 같은데 어떻게 생각하시나용?

Copy link
Member

Choose a reason for hiding this comment

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

오류 관련해서 슬랙 확인했는데 아마 token 안 넣어서 그런 것 같아용 슬랙으로 보내준 사진 보니까 token이 undefined로 넘어가더라구요!

백엔드가 token 유효검사 뺐다고 하니깐 그냥 두고 나중에 main 푸시할때만 빼도 될 것 같아용

Copy link
Member Author

Choose a reason for hiding this comment

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

조아용~ 제쪽에서두 토큰 뺐습니다! 첨에 메인 페이지 들어갈 때 토큰이 기본적으로 없으면 저런 오류나는 것 같아서 매번 이벤트 페이지 가서 토큰 넣어줘야 하는거 번거로웠는데 다행입니다 :)

Comment on lines 31 to 49
setStartDateTime(rushData.eventStartDate);
setEndDateTime(rushData.eventEndDate);

const events = rushData.events.map((event, idx) => {
const rushEvent = RUSH_EVENT_DATA[idx];
const eventEndTime = getMsTime(event.endDateTime);

return {
id: event.rushEventId,
date: event.startDateTime,
image: rushEvent?.image || "",
prizeName: rushEvent?.prizeName || "",
isPastEvent: serverDateTime > eventEndTime,
isTodayEvent:
event.rushEventId === rushData.todayEventId &&
serverDateTime <= eventEndTime,
};
});
setRushEvents(events);
Copy link
Member

Choose a reason for hiding this comment

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

요 데이터들 굳이 다른 state로 관리 안 하고 바로 가져다 써도 되지 않을까용?

Suggested change
setStartDateTime(rushData.eventStartDate);
setEndDateTime(rushData.eventEndDate);
const events = rushData.events.map((event, idx) => {
const rushEvent = RUSH_EVENT_DATA[idx];
const eventEndTime = getMsTime(event.endDateTime);
return {
id: event.rushEventId,
date: event.startDateTime,
image: rushEvent?.image || "",
prizeName: rushEvent?.prizeName || "",
isPastEvent: serverDateTime > eventEndTime,
isTodayEvent:
event.rushEventId === rushData.todayEventId &&
serverDateTime <= eventEndTime,
};
});
setRushEvents(events);
const { eventStartDate, eventEndDate, todayEventId, rushEventId, image, prizeName } = rushData;
const events = useMemo(() => rushData.events.map((event, idx) => {
const rushEvent = RUSH_EVENT_DATA[idx];
const eventEndTime = getMsTime(event.endDateTime);
return {
id: event.rushEventId,
date: event.startDateTime,
image: rushEvent?.image || "",
prizeName: rushEvent?.prizeName || "",
isPastEvent: serverDateTime > eventEndTime,
isTodayEvent:
event.rushEventId === rushData.todayEventId &&
serverDateTime <= eventEndTime,
};
}), [rushData])

어차피 useFetch가 반환하는 값이 state니까 따로 state 안 만들고 약간 요런식으로 해서 useEffect 내부가 아니라 컴포넌트 내부에서 바로 사용해도 괜찮지 않을까 싶어서요,,!

Copy link
Member Author

@sooyeoniya sooyeoniya Aug 18, 2024

Choose a reason for hiding this comment

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

반영했습니다! 유사하게 구현한 Main/Headline.tsx, Main/Lottery.tsx 부분에도 구조분해할당해서 바로 내부에서 호출하도록 변경했습니다! 굳이 state를 또 만들 필요는 없었던 것 같네용

Copy link

빌드를 성공했습니다! 🎉

Comment on lines +11 to +12
duration = 5000,
useDuration = true,
Copy link
Member

Choose a reason for hiding this comment

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

duration이 undefined인 경우 duration을 사용하지 않는다고 판단하면 되지 않나용? useDuration을 추가로 선언한 이유가 따로 있으신가욥?

Copy link
Member Author

Choose a reason for hiding this comment

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

그냥 toggle 함수만 필요한 부분에서는 duration이 없어야 하는데, duration이 기본값 5000으로 설정되어있으면 undefined인 경우로 처리가 안돼서 따로 boolean 값으로 설정해준거긴 합니다만, 아니면 아예 아래 코드처럼 해놓으면 useDuration이 필요 없을 것 같기도 하고여? toggle 함수만 사용할 부분에는 그냥 props 넘길 때 duration 자체를 -1로 두고, duration가 0보다 작을 때는 타이머가 작동 안하게 해도 될까요? 뭔가 -1이라는 것 자체가 맘에는 안들긴 하는데, 이렇게하면 잘 작동하는 것 같아용! (아예 props 넘길 때 duration 을 undefined로 설정하고 if 조건문에서 (duration !== undefined && duration > 0) 이렇게도 설정해봤는데, 이렇게 했는데두 5초 타이머가 작동 되네용 ㅠㅠ,, 이건 뭐가 문제인지...)

사실 duration 이 필요없는 부분에서는 그냥 toggle 함수만 필요하기 때문에 아예 컴포넌트 자체에서 그냥 toggle 함수 부분만 따로 빼서 적용해도 되는데, 괜히 훅으로 묶어서 써보겠다고 해서 골머리 아프네용 ㅠㅠ

어떤 방식이 더 좋을지 추천해주시면 감사하겠습니다 ! :)

제가 생각한 수정 코드

// 토글 함수만 사용하는 부분
const { toggleContents, toggle } = useToggleContents({ duration: -1 });
// 5초 타이머 동작 후 토글되는 로직이 필요한 부분
const { toggleContents } = useToggleContents();
import { useCallback, useEffect, useState } from "react";

interface UseToggleContentsProps {
    initialStatus?: boolean;
    duration?: number;
    // 요기 수정 (useDuration 제거)
}

export default function useToggleContents({
    initialStatus = true,
    duration = 5000,
}: UseToggleContentsProps = {}) {
    const [toggleContents, setToggleContents] = useState(initialStatus);

    const toggle = useCallback(() => {
        setToggleContents((prev) => !prev);
    }, []);

    useEffect(() => {
        if (duration && duration > 0) { // 요기 수정 (useDuration 제거)
            const timer = setTimeout(() => {
                toggle();
            }, duration);

            return () => clearTimeout(timer);
        }
    }, [duration, toggle]); // 요기 수정 (useDuration 제거)

    return { toggleContents, toggle };
}

Copy link
Member

Choose a reason for hiding this comment

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

아하 기본값 설정때매 undefined 처리가 안되는군용 그 부분을 생각 못했네욥,, 그럼 useDuration 사용해서 원래 코드처럼 붙기처리 하는게 맞을 것 같아용 근데 그러면 굳이 if 문에 duration 조건까지 넣어줄 필요는 없지 않을까요?? useDuration이 timer를 쓰겠다는 의미를 가지고 있으니까요!

    useEffect(() => {
        if (useDuration) {
            const timer = setTimeout(() => {
                toggle();
            }, duration);

            return () => clearTimeout(timer);
        }
    }, [useDuration, duration, toggle]);

요런식으로용

Copy link
Member Author

Choose a reason for hiding this comment

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

맞네용! 반영했고 동작 잘 되는거 확인했습니다!

Copy link

빌드를 성공했습니다! 🎉

@sooyeoniya sooyeoniya merged commit 3da2799 into dev Aug 18, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat 기능 구현
Projects
None yet
2 participants