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: 밸런스 게임 수정 및 삭제 로직 구현 #268

Open
wants to merge 50 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0af836b
feat: 라우팅 경로와 URL 생성 경로를 추가 합니다.
WonJuneKim Dec 17, 2024
7cf59d8
feat: 라우팅 경로를 App.tsx에 추가합니다.
WonJuneKim Dec 17, 2024
85705eb
fix: URL의 동적 파라미터를 api와의 일관성을 맞춥니다.
WonJuneKim Dec 17, 2024
10607db
feat: 밸런스게임 수정 api의 호출부를 생성합니다.
WonJuneKim Dec 17, 2024
328f672
feat: 밸런스게임 수정 커스텀 훅을 구현합니다.
WonJuneKim Dec 17, 2024
142b203
feat: 모달 호출을 위한 공용 훅을 생성합니다.
WonJuneKim Dec 18, 2024
8bbee08
feat: 컴포넌트가 이벤트 핸들러를 props로 받도록 합니다.
WonJuneKim Dec 18, 2024
2172ec9
feat: 모달 출력을 위한 태그를 생성합니다.
WonJuneKim Dec 18, 2024
230dc9b
feat: 모달 처리를 위한 이벤트 핸들러를 생성합니다.
WonJuneKim Dec 18, 2024
57648c7
fix: 스타일 파일의 네이밍 컨벤션을 수정합니다.
WonJuneKim Dec 19, 2024
50e9b66
feat: 게임 수정을 위한 데이터의 인터페이스를 관리하는 유틸리티 함수를 구현합니다.
WonJuneKim Dec 19, 2024
f2998fd
refactor: 페이지의 모달을 공통 훅을 통해 관리하도록 수정합니다.
WonJuneKim Dec 19, 2024
250dd47
refactor: 이미지 삭제 시 fileId는 0이 아닌 null로 처리합니다.
WonJuneKim Dec 19, 2024
591c48e
feat: 이미지 삭제 취소를 위한 핸들러를 복원합니다.
WonJuneKim Dec 19, 2024
3b4e59e
feat: 밸런스 게임 생성 시 이미지 처리를 담당하는 훅을 생성합니다.
WonJuneKim Dec 19, 2024
9a3a3f5
refactor: 범용성을 위해 공통적이지 않은 props를 옵셔널로 선언합니다.
WonJuneKim Dec 19, 2024
697727d
feat: 수정 기능을 수행하는 props를 옵셔널로 추가합니다.
WonJuneKim Dec 19, 2024
88a2ca8
feat: 수정 기능을 수행하는 버튼을 위한 스타일링을 추가합니다.
WonJuneKim Dec 19, 2024
cf4c86d
feat: 수정 시와 생성 시 스토리북 테스트를 수행합니다.
WonJuneKim Dec 19, 2024
cff53a0
feat: 밸런스 게임 조회 시 송신 받는 인터페이스 항목을 추가합니다.
WonJuneKim Dec 19, 2024
6685cf9
refactor: 게임 데이터 변환 시 imgUrl과 fileId도 같이 변환하도록 수정합니다.
WonJuneKim Dec 19, 2024
eb478d9
refactor: 중복되는 상태 및 useEffect를 제거합니다.
WonJuneKim Dec 19, 2024
caa7c96
refactor: 게임이 삭제되는 로직을 명시적으로 수정합니다.
WonJuneKim Dec 19, 2024
e470ab1
refactor: 컴포넌트의 이벤트는 더 이상 훅을 통해 관리 되지 않습니다.
WonJuneKim Dec 19, 2024
49aee76
move: 사용하지 않는 커스텀 훅 삭제
WonJuneKim Dec 19, 2024
3779d92
refactor: 게임 관련 상태는 부모인 페이지에서 관리하도록 수정합니다.
WonJuneKim Dec 19, 2024
95a788b
feat: 밸런스 게임 수정 페이지를 구현합니다.
WonJuneKim Dec 19, 2024
0358510
feat: 알림 메시지를 상수화 합니다.
WonJuneKim Dec 19, 2024
1e4812a
refactor: 태그 모달이 기본 상태를 가질 수 있도록 구조를 변경합니다.
WonJuneKim Dec 19, 2024
022eda8
fix: 잘못 선언된 콜백 함수를 외부로 분리합니다.
WonJuneKim Dec 19, 2024
c78bc24
feat: 요구되는 토스트 모달 메시지를 상수화합니다.
WonJuneKim Dec 19, 2024
69911a8
fix: 상수화된 메시지 네이밍 수정을 반영합니다.
WonJuneKim Dec 19, 2024
0472717
feat: 밸런스게임 수정 완료 핸들러를 추가합니다.
WonJuneKim Dec 19, 2024
f92e414
feat: 게임 삭제 관련 이벤트 메시지를 상수화 합니다.
WonJuneKim Dec 19, 2024
d3e4134
feat: 밸런스 게임 삭제 api 호출부를 작성합니다.
WonJuneKim Dec 19, 2024
8b6707c
feat: 밸런스 게임 삭제 커스텀 훅을 작성합니다.
WonJuneKim Dec 19, 2024
4079134
feat: 토스트 모달 출력을 위한 태그를 추가합니다.
WonJuneKim Dec 19, 2024
2b75ce2
feat: 밸런스 게임 삭제 로직을 구현합니다.
WonJuneKim Dec 19, 2024
ee4d849
fix: 스토리북 테스트 파일을 변경된 컴포넌트에 맞춰 수정합니다.
WonJuneKim Dec 19, 2024
58211a0
fix: 안전성을 위해 nullish 연산자를 사용합니다.
WonJuneKim Dec 19, 2024
aed33b6
fix: 게임 수정 페이지의 라우트를 보호된 영역으로 이동합니다.
WonJuneKim Dec 19, 2024
1afd53c
fix: void를 명시한 부분을 제거합니다.
WonJuneKim Dec 19, 2024
1595b75
refactor: api 요청 함수 네이밍을 HTTP 메서드 기반으로 통일
WonJuneKim Dec 29, 2024
178f4ed
refactor: api response의 데이터를 다루는 커스텀 훅의 이름을 비즈니스 로직에 적합하게 수정합니다.
WonJuneKim Dec 29, 2024
969b9ca
refactor: 전체 게임 스테이지를 관리하는 상수를 분리합니다.
WonJuneKim Dec 29, 2024
c0a54ef
refactor: 라우팅 상수를 그룹화하고, 동적 경로로 통일합니다.
WonJuneKim Dec 30, 2024
efbfa97
refactor: 변경된 라우팅 경로를 navigate 함수에 적용합니다.
WonJuneKim Dec 30, 2024
10eec60
Merge branch 'dev' into feat/231-balancegame-edit
WonJuneKim Dec 30, 2024
c5bedf4
move: 병합 해결시 삭제되지 않았던 불필요한 코드를 제거합니다.
WonJuneKim Dec 30, 2024
ce3fe30
feat: 권한이 없는 사용자가 url로 접근 시에도 접근을 차단합니다.
WonJuneKim Dec 30, 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
8 changes: 6 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Route, Routes } from 'react-router-dom';
import MyPage from '@/pages/MyPage/MyPage';
import SearchGamePage from '@/pages/SearchResultsPage/SearchGamePage';
import SearchTalkPickPage from '@/pages/SearchResultsPage/SearchTalkPickPage';
import BalanceGameEditPage from '@/pages/BalanceGameEditPage/BalanceGameEditPage';
import ProtectedRoutes from './components/Routes/ProtectedRoutes';
import { PATH } from './constants/path';
import { useTokenRefresh } from './hooks/common/useTokenRefresh';
Expand Down Expand Up @@ -78,10 +79,9 @@ const App: React.FC = () => {
<Route path={PATH.TALKPICK_PLACE} element={<TalkPickPlacePage />} />
<Route path={PATH.TALKPICK()} element={<TalkPickPage />} />
<Route
path={PATH.BALANCEGAME()}
path={PATH.BALANCEGAME.VIEW()}
element={isMobile ? <BalanceGameMobilePage /> : <BalanceGamePage />}
/>

{/* <Route path="/search" element={<SearchResultsPage />} /> */}
{/* <Route path="posts" element={<PostList />} />
<Route path="posts/:id" element={<PostPage />} />
Expand All @@ -108,6 +108,10 @@ const App: React.FC = () => {
path={PATH.CREATE.GAME}
element={<BalanceGameCreationPage />}
/>
<Route
path={PATH.BALANCEGAME.EDIT()}
element={<BalanceGameEditPage />}
/>
<Route
path={PATH.CHANGE.PROFILE}
element={<ChangeUserInfoPage />}
Expand Down
12 changes: 12 additions & 0 deletions src/api/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export const getGameBySetId = async (gameSetId: Id) => {
return data;
};

export const putGameBySetId = async (gameSetId: Id, gameData: BalanceGame) => {
const { data } = await axiosInstance.put<BalanceGame>(
END_POINT.GAME_SET(gameSetId),
gameData,
);
return data;
};

export const deleteBySetId = async (gameSetId: Id) => {
return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
};
Comment on lines +52 to +54
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 API의 응답 처리를 명확히 해주세요.

삭제 API의 응답 처리가 불명확합니다. 성공/실패 여부를 명확하게 반환하도록 개선이 필요합니다.

-export const deleteBySetId = async (gameSetId: Id) => {
-  return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
+export const deleteBySetId = async (gameSetId: Id): Promise<boolean> => {
+  try {
+    await axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
+    return true;
+  } catch (error) {
+    console.error('게임 삭제 중 오류 발생:', error);
+    return false;
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const deleteBySetId = async (gameSetId: Id) => {
return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
};
export const deleteBySetId = async (gameSetId: Id): Promise<boolean> => {
try {
await axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
return true;
} catch (error) {
console.error('게임 삭제 중 오류 발생:', error);
return false;
}
};


export const putGame = async (gameId: Id, gameData: Game) => {
const { data } = await axiosInstance.put<GameContent>(
END_POINT.EDIT_GAME(gameId),
Expand Down
23 changes: 19 additions & 4 deletions src/components/molecules/TagModal/TagModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import Modal from '@/components/atoms/Modal/Modal';
import Button from '@/components/atoms/Button/Button';
import Input from '@/components/atoms/Input/Input';
Expand All @@ -9,11 +9,26 @@ interface TagModalProps {
isOpen: boolean;
onClose: () => void;
onTagSubmit: (mainTag: string, subTag: string) => void;
initialMainTag?: string;
initialSubTag?: string;
}

const TagModal = ({ isOpen, onClose, onTagSubmit }: TagModalProps) => {
const [mainTag, setMainTag] = useState<string | null>(null);
const [subTag, setSubTag] = useState('');
const TagModal = ({
isOpen,
onClose,
onTagSubmit,
initialMainTag,
initialSubTag,
}: TagModalProps) => {
const [mainTag, setMainTag] = useState<string | null>(initialMainTag ?? null);
const [subTag, setSubTag] = useState<string>(initialSubTag ?? '');

useEffect(() => {
if (isOpen) {
setMainTag(initialMainTag ?? null);
setSubTag(initialSubTag ?? '');
}
}, [isOpen, initialMainTag, initialSubTag]);

const handleTagSubmit = () => {
if (mainTag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ export const toastModalStyling = css({
transform: 'translate(-50%)',
zIndex: '1000',
});

export const titleDescriptionFieldContainer = css({
position: 'relative',
});

export const tagEditButtonContainer = css({
position: 'absolute',
top: '16px',
right: '21px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '50',
});
170 changes: 131 additions & 39 deletions src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,157 @@
import React, { useEffect } from 'react';
import React, { useState } from 'react';
import TitleDescriptionField from '@/components/atoms/TitleDescriptionField/TitleDescriptionField';
import BalanceGameOptionCard from '@/components/molecules/BalanceGameOptionCard/BalanceGameOptionCard';
import DraftPostButton from '@/components/atoms/DraftPostButton/DraftPostButton';
import { BalanceGameSet } from '@/types/game';
import { useBalanceGameCreation } from '@/hooks/game/useBalanceGameCreation';
import { BalanceGameOption, BalanceGameSet } from '@/types/game';
import GameNavigationSection from '@/components/molecules/GameNavigationSection/GameNavigationSection';
import useToastModal from '@/hooks/modal/useToastModal';
import ToastModal from '@/components/atoms/ToastModal/ToastModal';
import Button from '@/components/atoms/Button/Button';
import { ERROR } from '@/constants/message';
import * as S from './BalanceGameCreation.style';

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

const BalanceGameCreation = ({
title,
onTitleChange,
handleCompleteClick,
onDraftLoad,
onGamesUpdate,
games,
onGamesChange,
onImageChange,
onImageDelete,
loadedGames,
handleTagEditClick,
}: BalanceGameCreationProps) => {
const totalStage = 10;
const { isVisible, modalText, showToastModal } = useToastModal();

const {
games,
currentStage,
currentOptions,
currentDescription,
clearInput,
handleNextStage,
handlePrevStage,
handleStageDescriptionChange,
handleOptionUpdate,
} = useBalanceGameCreation(showToastModal, totalStage, loadedGames);

useEffect(() => {
onGamesUpdate(games);
}, [games, onGamesUpdate]);
const [currentStage, setCurrentStage] = useState(0);
const [clearInput, setClearInput] = useState(false);

const currentGame = games[currentStage];
const currentOptions = currentGame?.gameOptions || [];
const currentDescription = currentGame?.description || '';

const updateOption = (
stageIndex: number,
optionType: 'A' | 'B',
newOption: Partial<BalanceGameOption>,
) => {
const updatedGames = games.map((game, idx) =>
idx === stageIndex
? {
...game,
gameOptions: game.gameOptions.map((opt, optIdx) => {
const isA = optionType === 'A' && optIdx === 0;
const isB = optionType === 'B' && optIdx === 1;
if (isA || isB) {
return { ...opt, ...newOption };
}
return opt;
}),
}
: game,
);
onGamesChange(updatedGames);
};

const validateStage = (): true | string => {
const { gameOptions } = currentGame || { gameOptions: [] };

if (!gameOptions[0]?.name.trim() || !gameOptions[1]?.name.trim()) {
return ERROR.VALIDATE.OPTION;
}

const hasBothImages =
!!gameOptions[0]?.imgUrl.trim() && !!gameOptions[1]?.imgUrl.trim();
const hasNoImages =
!gameOptions[0]?.imgUrl.trim() && !gameOptions[1]?.imgUrl.trim();

if (!(hasBothImages || hasNoImages)) {
return ERROR.VALIDATE.GAME_IMAGE;
}

return true;
};

const handleNextStage = () => {
const validationResult = validateStage();
if (currentStage < games.length - 1) {
if (validationResult === true) {
setClearInput(true);
setCurrentStage((prev) => prev + 1);
} else {
showToastModal(validationResult);
}
}
};

const handlePrevStage = () => {
if (currentStage > 0) {
setClearInput(true);
setCurrentStage((prev) => prev - 1);
}
};

const handleStageDescriptionChange = (newDescription: string) => {
const updatedGames = games.map((game, idx) =>
idx === currentStage ? { ...game, description: newDescription } : game,
);
onGamesChange(updatedGames);
};

const handleOptionUpdate = (
optionType: 'A' | 'B',
field: 'name' | 'description',
value: string,
) => {
updateOption(currentStage, optionType, { [field]: value });
};

return (
<div css={S.pageContainer}>
<TitleDescriptionField
title={title}
description={currentDescription}
onTitleChange={onTitleChange}
onDescriptionChange={(e) =>
handleStageDescriptionChange(e.target.value)
}
/>
<div css={S.titleDescriptionFieldContainer}>
<TitleDescriptionField
title={title}
description={currentDescription}
onTitleChange={onTitleChange}
onDescriptionChange={(e) =>
handleStageDescriptionChange(e.target.value)
}
/>
{handleTagEditClick && (
<div css={S.tagEditButtonContainer}>
<Button
size="large"
variant="outlinePrimary"
onClick={handleTagEditClick}
>
태그 수정
</Button>
</div>
)}
</div>
<div css={S.optionsContainer}>
<BalanceGameOptionCard
option="A"
imgUrl={currentOptions[0]?.imgUrl || ''}
onImageChange={(file) => onImageChange(currentStage, 0, file)}
onImageChange={(file) => {
onImageChange(currentStage, 0, file);
}}
onImageDelete={() => onImageDelete(currentStage, 0)}
choiceInputProps={{
value: currentOptions[0]?.name || '',
Expand All @@ -79,7 +167,9 @@ const BalanceGameCreation = ({
<BalanceGameOptionCard
option="B"
imgUrl={currentOptions[1]?.imgUrl || ''}
onImageChange={(file) => onImageChange(currentStage, 1, file)}
onImageChange={(file) => {
onImageChange(currentStage, 1, file);
}}
onImageDelete={() => onImageDelete(currentStage, 1)}
choiceInputProps={{
value: currentOptions[1]?.name || '',
Expand All @@ -93,13 +183,15 @@ const BalanceGameCreation = ({
clearInput={clearInput}
/>
</div>
<div css={S.draftPostButtonContainer}>
<DraftPostButton onClick={onDraftLoad} />
</div>
{onDraftLoad && (
<div css={S.draftPostButtonContainer}>
<DraftPostButton onClick={onDraftLoad} />
</div>
)}
<div css={S.navigationContainer}>
<GameNavigationSection
currentStage={currentStage}
totalStage={totalStage}
totalStage={games.length}
handleNextClick={handleNextStage}
handlePrevClick={handlePrevStage}
handleCompleteClick={handleCompleteClick}
Expand Down
3 changes: 2 additions & 1 deletion src/components/organisms/BalanceGameList/BalanceGameList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GameContent } from '@/types/game';
import { ToggleGroupValue } from '@/types/toggle';
import { useNavigate } from 'react-router-dom';
import { ERROR } from '@/constants/message';
import { PATH } from '@/constants/path';
import MobileToggleGroup from '@/components/mobile/atoms/MobileToggleGroup/MobileToggleGroup';
import * as S from './BalanceGameList.style';

Expand Down Expand Up @@ -41,7 +42,7 @@ const BalanceGameList = ({
alert(ERROR.GAME.NOT_EXIST);
return;
}
navigate(`/balancegame/${gameId}`);
navigate(`/${PATH.BALANCEGAME.VIEW(gameId)}`);
},
[navigate],
);
Expand Down
Loading
Loading