Skip to content

Commit

Permalink
feat: 밸런스 게임 수정 및 삭제 로직 구현 (#268)
Browse files Browse the repository at this point in the history
* feat: 라우팅 경로와 URL 생성 경로를 추가 합니다.

* feat: 라우팅 경로를 App.tsx에 추가합니다.

* fix: URL의 동적 파라미터를 api와의 일관성을 맞춥니다.

* feat: 밸런스게임 수정 api의 호출부를 생성합니다.

* feat: 밸런스게임 수정 커스텀 훅을 구현합니다.

* feat: 모달 호출을 위한 공용 훅을 생성합니다.

* feat: 컴포넌트가 이벤트 핸들러를 props로 받도록 합니다.

* feat: 모달 출력을 위한 태그를 생성합니다.

* feat: 모달 처리를 위한 이벤트 핸들러를 생성합니다.

* fix: 스타일 파일의 네이밍 컨벤션을 수정합니다.

* feat: 게임 수정을 위한 데이터의 인터페이스를 관리하는 유틸리티 함수를 구현합니다.

* refactor: 페이지의 모달을 공통 훅을 통해 관리하도록 수정합니다.

* refactor: 이미지 삭제 시 fileId는 0이 아닌 null로 처리합니다.

* feat: 이미지 삭제 취소를 위한 핸들러를 복원합니다.

* feat: 밸런스 게임 생성 시 이미지 처리를 담당하는 훅을 생성합니다.

* refactor: 범용성을 위해 공통적이지 않은 props를 옵셔널로 선언합니다.

* feat: 수정 기능을 수행하는 props를 옵셔널로 추가합니다.

* feat: 수정 기능을 수행하는 버튼을 위한 스타일링을 추가합니다.

* feat: 수정 시와 생성 시 스토리북 테스트를 수행합니다.

* feat: 밸런스 게임 조회 시 송신 받는 인터페이스 항목을 추가합니다.

* refactor: 게임 데이터 변환 시 imgUrl과 fileId도 같이 변환하도록 수정합니다.

* refactor: 중복되는 상태 및 useEffect를 제거합니다.

* refactor: 게임이 삭제되는 로직을 명시적으로 수정합니다.

* refactor: 컴포넌트의 이벤트는 더 이상 훅을 통해 관리 되지 않습니다.

* move: 사용하지 않는 커스텀 훅 삭제

* refactor: 게임 관련 상태는 부모인 페이지에서 관리하도록 수정합니다.

* feat: 밸런스 게임 수정 페이지를 구현합니다.

* feat: 알림 메시지를 상수화 합니다.

* refactor: 태그 모달이 기본 상태를 가질 수 있도록 구조를 변경합니다.

* fix: 잘못 선언된 콜백 함수를 외부로 분리합니다.

* feat: 요구되는 토스트 모달 메시지를 상수화합니다.

* fix: 상수화된 메시지 네이밍 수정을 반영합니다.

* feat: 밸런스게임 수정 완료 핸들러를 추가합니다.

* feat: 게임 삭제 관련 이벤트 메시지를 상수화 합니다.

* feat: 밸런스 게임 삭제 api 호출부를 작성합니다.

* feat: 밸런스 게임 삭제 커스텀 훅을 작성합니다.

* feat: 토스트 모달 출력을 위한 태그를 추가합니다.

* feat: 밸런스 게임 삭제 로직을 구현합니다.

* fix: 스토리북 테스트 파일을 변경된 컴포넌트에 맞춰 수정합니다.

* fix: 안전성을 위해 nullish 연산자를 사용합니다.

* fix: 게임 수정 페이지의 라우트를 보호된 영역으로 이동합니다.

* fix: void를 명시한 부분을 제거합니다.

* refactor: api 요청 함수 네이밍을 HTTP 메서드 기반으로 통일

* refactor: api response의 데이터를 다루는 커스텀 훅의 이름을 비즈니스 로직에 적합하게 수정합니다.

* refactor: 전체 게임 스테이지를 관리하는 상수를 분리합니다.

* refactor: 라우팅 상수를 그룹화하고, 동적 경로로 통일합니다.

* refactor: 변경된 라우팅 경로를 navigate 함수에 적용합니다.

* move: 병합 해결시 삭제되지 않았던 불필요한 코드를 제거합니다.

* feat: 권한이 없는 사용자가 url로 접근 시에도 접근을 차단합니다.

* fix: 병합과정에서 생긴 organisms 컴포넌트 내의 에러를 수정합니다.

* fix: 병합과정에서 생긴 pages 내의 에러를 수정합니다.

* move: 중복되는 import 문을 제거합니다.
  • Loading branch information
WonJuneKim authored Feb 2, 2025
1 parent 6a5bdee commit 536986c
Show file tree
Hide file tree
Showing 23 changed files with 1,129 additions and 622 deletions.
7 changes: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Route, Routes, useLocation, useNavigate } 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 @@ -74,7 +75,7 @@ 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 />} /> */}
Expand Down Expand Up @@ -109,6 +110,10 @@ const App: React.FC = () => {
)
}
/>
<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));
};

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

0 comments on commit 536986c

Please sign in to comment.