Skip to content

Commit

Permalink
feat: 밸런스 게임 임시 저장 (#242)
Browse files Browse the repository at this point in the history
* refactor: 임시 저장 인터페이스 수정 및 사용하지 않는 인터페이스 임시 주석 처리

* move: 불필요한 주석 제거

* refactor: description 관리를 하위 컴포넌트에서 처리하도록 수정

* refactor: stage 관리를 하위 컴포넌트에서만 수행하도록 설정

* refactor: 스테이지 처리를 컴포넌트 내부에서만 처리하고 이벤트를 전달하지 않도록 수정

* refactor: 이미지 업로드 시 setIsUploadingImage props를 옵셔널로 처리

* feat: 밸런스 게임 임시 저장 로직 구현

* fix: 사용하지 않는 api 관련 코드 주석 처리

* feat: 임시 저장 로깅을 위한 콘솔 로그 추가

* fix: api 응답에 맞춘 인터페이스 isLoaded을 옵셔널로 추가

* feat: 임시 저장 불러오기 api 재설정

* move: 사용하지 않는 이전 인터페이스 제거

* fix: 임시 저장 불러오기 훅의 이름을 조금 더 구체적으로 설정

* feat: 임시 저장 api 재연동

* fix: 임시 저장 데이터 타입과 호환을 위해 fileI에 null 허용

* feat: 임시 저장 불러오기 처리 시 데이터를 가공할 수 있도록 loadedGames props 추가

* feat: 임시 저장 데이터가 들어오면 화면을 리렌더링 하도록 수정

* feat: 디버깅을 위한 콘솔 로그 임시 추가

* fix: 상위 스코프에 선언된 변수 이름과 충돌을 방지하기 위해 변수 이름 수정

* feat: 모달 종류 별 배경 분리

* move: 사용하지 않는 import 문 제거

* feat: 임시 저장 데이터 전처리 함수명을 수정

* fix: 임시 저장 데이터 변환 시 falsy 값을 체크하지 않고 null 과 undefined만 처리하도록 수정

* refactor: loadedGames의 초기상태를 null로 더 명확하게 명시

* fix: 사용자 액션 후에 alert 대신 toastModal을 사용하도록 수정
  • Loading branch information
WonJuneKim authored Dec 2, 2024
1 parent 655ee5e commit fbe593b
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 118 deletions.
5 changes: 1 addition & 4 deletions src/api/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
GameContent,
GameSet,
TempGame,
TempGameResponse,
} from '@/types/game';
import { Id } from '@/types/api';
import { axiosInstance } from './interceptor';
Expand All @@ -21,9 +20,7 @@ export const postTempGame = async (tempGameData: TempGame) => {
};

export const getTempGame = async () => {
const { data } = await axiosInstance.get<TempGameResponse>(
END_POINT.TEMP_GAME,
);
const { data } = await axiosInstance.get<TempGame>(END_POINT.TEMP_GAME);
return data;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,24 @@ import * as S from './BalanceGameCreation.style';

export interface BalanceGameCreationProps {
title: string;
description?: string;
onTitleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onDescriptionChange: (description: string) => void;
handleCompleteClick: () => void;
onDraftLoad: () => void;
onStageChange: (stage: number) => void;
onGamesUpdate: (games: BalanceGameSet[]) => void;
onImageChange: (stageIndex: number, optionIndex: number, file: File) => void;
onImageDelete: (stageIndex: number, optionIndex: number) => void;
loadedGames?: BalanceGameSet[];
}

const BalanceGameCreation = ({
title,
description,
onTitleChange,
onDescriptionChange,
handleCompleteClick,
onDraftLoad,
onStageChange,
onGamesUpdate,
onImageChange,
onImageDelete,
loadedGames,
}: BalanceGameCreationProps) => {
const totalStage = 10;

Expand All @@ -45,12 +41,7 @@ const BalanceGameCreation = ({
handleOptionChange,
handleDescriptionChange,
handleStageDescriptionChange,
} = useBalanceGameCreation(totalStage, currentStage);

useEffect(() => {
onStageChange(currentStage);
onDescriptionChange(currentDescription);
}, [currentStage, onStageChange, currentDescription, onDescriptionChange]);
} = useBalanceGameCreation(currentStage, loadedGames, totalStage);

useEffect(() => {
if (currentOptions.length > 0) {
Expand All @@ -62,7 +53,7 @@ const BalanceGameCreation = ({
<div css={S.pageContainer}>
<TitleDescriptionField
title={title}
description={description}
description={currentDescription}
onTitleChange={onTitleChange}
onDescriptionChange={(e) =>
handleStageDescriptionChange(e.target.value)
Expand Down
11 changes: 9 additions & 2 deletions src/hooks/api/file/useFileUploadMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import { postFile } from '@/api/file';
import { UploadedImage, FileUploadType } from '@/types/file';

export const useFileUploadMutation = (
setIsUploadingImage: React.Dispatch<React.SetStateAction<boolean>>,
setIsUploadingImage?: React.Dispatch<React.SetStateAction<boolean>>,
) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (data: { formData: FormData; params: FileUploadType }) =>
postFile(data.formData, data.params),
onMutate: () => {
setIsUploadingImage(true);
if (setIsUploadingImage) {
setIsUploadingImage(true);
}
},
onSuccess: async (response: UploadedImage) => {
await queryClient.invalidateQueries({
queryKey: ['uploadedFiles'],
});
},
onSettled: () => {
if (setIsUploadingImage) {
setIsUploadingImage(false);
}
},
});
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { TempGame } from '@/types/game';
import { getTempGame } from '@/api/game';
import { TempGameResponse } from '@/types/game';

export const useTempGameQuery = () => {
return useQuery<TempGameResponse>({
export const useLoadTempGameQuery = () => {
return useQuery<TempGame>({
queryKey: ['tempGame'],
queryFn: getTempGame,
enabled: false,
staleTime: 5 * 60 * 1000,
});
};
24 changes: 24 additions & 0 deletions src/hooks/api/game/useSaveTempGameMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postTempGame } from '@/api/game';
import { TempGame } from '@/types/game';

export const useSaveTempGameMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (tempGameData: TempGame) => {
console.log('임시 저장 요청 데이터:', tempGameData);
return postTempGame(tempGameData);
},
onSuccess: async (response) => {
console.log('임시 저장 성공 응답:', response);
await queryClient.invalidateQueries({
queryKey: ['tempGame'],
});
console.log('캐시 무효화 완료');
},
onError: (error) => {
console.error('임시 저장 실패:', error);
},
});
};
28 changes: 0 additions & 28 deletions src/hooks/api/game/useTempGameSaveMutation.ts

This file was deleted.

11 changes: 9 additions & 2 deletions src/hooks/game/useBalanceGameCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import {
} from '@/utils/balanceGameUtils';

export const useBalanceGameCreation = (
totalStage: number = 10,
currentStage: number,
loadedGames?: BalanceGameSet[],
totalStage: number = 10,
) => {
const [games, setGames] = useState<BalanceGameSet[]>(
createInitialGameStages(totalStage),
loadedGames || createInitialGameStages(totalStage),
);
const [currentOptions, setCurrentOptions] = useState<BalanceGameOption[]>([]);
const [currentDescription, setCurrentDescription] = useState<string>('');

useEffect(() => {
if (loadedGames) {
setGames(loadedGames);
}
}, [loadedGames]);

useEffect(() => {
const stage = games[currentStage] || { gameOptions: [], description: '' };
setCurrentOptions(stage.gameOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const customButtonStyle = css({
height: '60px',
});

export const modalBackdrop = css({
export const submitModalBackdrop = css({
position: 'fixed',
top: 0,
left: 0,
Expand All @@ -51,6 +51,15 @@ export const modalBackdrop = css({
zIndex: '900',
});

export const deleteModalBackdrop = css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: '900',
});

export const centerStyling = css({
position: 'fixed',
top: '50%',
Expand Down
95 changes: 65 additions & 30 deletions src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,68 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import BalanceGameCreation from '@/components/organisms/BalanceGameCreation/BalanceGameCreation';
import Button from '@/components/atoms/Button/Button';
import Divider from '@/components/atoms/Divider/Divider';
import { BalanceGame, BalanceGameSet } from '@/types/game';
import {
BalanceGame,
BalanceGameSet,
TempGame,
TempGameSet,
} from '@/types/game';
import { useFileUploadMutation } from '@/hooks/api/file/useFileUploadMutation';
import ToastModal from '@/components/atoms/ToastModal/ToastModal';
import TagModal from '@/components/molecules/TagModal/TagModal';
import { useCreateBalanceGameMutation } from '@/hooks/api/game/useCreateBalanceGameMutation';
import useToastModal from '@/hooks/modal/useToastModal';
import TextModal from '@/components/molecules/TextModal/TextModal';
import { useNavigate } from 'react-router-dom';
import { useSaveTempGameMutation } from '@/hooks/api/game/useSaveTempGameMutation';
import { createInitialGameStages } from '@/utils/balanceGameUtils';
import { useLoadTempGameQuery } from '@/hooks/api/game/useLoadTempGameQuery';
import * as S from './BalanceGameCreationPage.style';

const BalanceGameCreationPage = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [games, setGames] = useState<BalanceGameSet[]>([]);
const [isTagModalOpen, setIsTagModalOpen] = useState(false);
const [isTextModalOpen, setIsTextModalOpen] = useState(false);
const [popupData, setPopupData] = useState<{
stageIndex: number;
optionIndex: number;
} | null>(null);
const [activeStage, setActiveStage] = useState(0);
const navigate = useNavigate();

const { handleCreateBalanceGame } = useCreateBalanceGameMutation();
const { mutateAsync: uploadImage } = useFileUploadMutation();
const { mutate: saveTempGame } = useSaveTempGameMutation();
const { data: tempGame, isSuccess } = useLoadTempGameQuery();
const { isVisible, modalText, showToastModal } = useToastModal();
const [loadedGames, setLoadedGames] = useState<BalanceGameSet[] | null>(null);

const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
};

const handleDescriptionChange = (newDescription: string) => {
setDescription(newDescription);
setGames((prevGames) => {
const updatedGames = [...prevGames];
updatedGames[activeStage] = {
...updatedGames[activeStage],
description,
};
return updatedGames;
});
};

const handleDraftLoad = () => {
alert('준비 중입니다!');
if (isSuccess && tempGame) {
console.log('임시 저장 불러오기 시작');
console.log('불러온 임시 저장 데이터:', tempGame);
const initialStages = createInitialGameStages(10);
console.log('생성된 초기 스테이지 데이터:', initialStages);

const mappedGames: BalanceGameSet[] = initialStages.map((stage, idx) => ({
description: tempGame.tempGames[idx]?.description || stage.description,
gameOptions: stage.gameOptions.map((option, optionIdx) => ({
...option,
...tempGame.tempGames[idx]?.tempGameOptions[optionIdx],
})),
}));
console.log('병합된 게임 데이터:', mappedGames);

setTitle(tempGame.title);
setLoadedGames(mappedGames);
showToastModal('임시 저장 데이터를 불러왔습니다!');
} else {
showToastModal('임시 저장 데이터를 불러오는 데 실패했습니다.');
}
};

const handleImageUpload = async (
Expand Down Expand Up @@ -153,15 +169,36 @@ const BalanceGameCreationPage = () => {
}
};

const handleSaveDraft = () => {
alert('준비 중입니다!');
const convertToTempGameSets = (gameSets: BalanceGameSet[]): TempGameSet[] => {
return gameSets.map(({ description: setDescription, gameOptions }) => ({
description: setDescription,
tempGameOptions: gameOptions.map(
({ name, description: optionDescription, fileId, optionType }) => ({
name,
description: optionDescription,
fileId: fileId ?? null,
optionType,
}),
),
}));
};

useEffect(() => {
if (games[activeStage]) {
setDescription(games[activeStage].description || '');
}
}, [activeStage, games]);
const handleSaveDraft = () => {
const tempGameData: TempGame = {
title,
isLoaded: false,
tempGames: convertToTempGameSets(games),
};
console.log('임시 저장 실행 데이터:', tempGameData);
saveTempGame(tempGameData, {
onSuccess: () => {
showToastModal('임시 저장이 완료되었습니다!');
},
onError: () => {
showToastModal('임시 저장에 실패했습니다. 다시 시도해주세요.');
},
});
};

return (
<div css={S.PageContainer}>
Expand All @@ -173,15 +210,13 @@ const BalanceGameCreationPage = () => {
<Divider orientation="width" length={1175} />
<BalanceGameCreation
title={title}
description={description}
onTitleChange={handleTitleChange}
onDescriptionChange={handleDescriptionChange}
handleCompleteClick={handleCompleteClick}
onDraftLoad={handleDraftLoad}
onStageChange={(stage) => setActiveStage(stage)}
onGamesUpdate={(updatedGames) => setGames(updatedGames)}
onImageChange={onImageChange}
onImageDelete={onImageDelete}
loadedGames={loadedGames || undefined}
/>
<div css={S.buttonContainer}>
<Button
Expand All @@ -195,7 +230,7 @@ const BalanceGameCreationPage = () => {
</div>
{isTagModalOpen && (
<>
<div css={S.modalBackdrop} />
<div css={S.submitModalBackdrop} />
<div css={S.centerStyling}>
<TagModal
isOpen={isTagModalOpen}
Expand All @@ -207,7 +242,7 @@ const BalanceGameCreationPage = () => {
)}
{isTextModalOpen && (
<>
<div css={S.modalBackdrop} />
<div css={S.deleteModalBackdrop} />
<div css={S.centerStyling}>
<TextModal
text="이미지를 삭제하시겠습니까?"
Expand Down
Loading

0 comments on commit fbe593b

Please sign in to comment.