diff --git a/src/App.tsx b/src/App.tsx index f2986c73..a16d4d71 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -148,7 +148,7 @@ const App: React.FC = () => { /> } + element={!isMobile && } /> { - if (currentMainTag) { - submitGame(); - onClose?.(); - } + if (!currentMainTag) return; + + const { isValid } = validateGameTag(form); + if (!isValid) return; + + submitGame(); + onClose?.(); }; return ( @@ -70,7 +74,6 @@ const GameTagModal = ({ name="subTag" css={S.inputStyling} placeholder="ex. 너무어려운밸런스게임, 선택장애, 이상형" - maxLength={10} value={form.subTag} onChange={setSubTagValue} /> diff --git a/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx b/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx index 0edca8eb..580d817c 100644 --- a/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx +++ b/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { BUTTON_TEXT, MAX_STAGE } from '@/constants/game'; +import { GameSet } from '@/types/game'; import ToastModal from '@/components/atoms/ToastModal/ToastModal'; import Button from '@/components/mobile/atoms/Button/Button'; import GameStageLabel from '@/components/mobile/atoms/GameStageLabel/GameStageLabel'; @@ -12,7 +13,15 @@ import TempGameModal from '@/components/mobile/molecules/TempGameModal/TempGameM import { usePostBalanceGameForm } from '@/hooks/game/usePostBalanceGameForm'; import * as S from './BalanceGameCreateSection.style'; -const BalanceGameCreateSection = () => { +interface BalanceGameCreateSectionProps { + existingGame?: GameSet; + gameSetId?: number; +} + +const BalanceGameCreateSection = ({ + existingGame, + gameSetId, +}: BalanceGameCreateSectionProps) => { const [gameStage, setGameStage] = useState(0); const [tagModalOpen, setTagModalOpen] = useState(false); @@ -34,7 +43,13 @@ const BalanceGameCreateSection = () => { handleBalanceGame, handleTempBalanceGame, handleDraftButton, - } = usePostBalanceGameForm(gameStage, setGameStage, setTagModalOpen); + } = usePostBalanceGameForm( + gameStage, + setGameStage, + setTagModalOpen, + existingGame, + gameSetId, + ); return (
@@ -79,7 +94,8 @@ const BalanceGameCreateSection = () => { }} onConfirm={() => { handleDeleteImg( - form.games[gameStage].gameOptions[selectedOptionId].fileId, + form.games[gameStage].gameOptions[selectedOptionId].fileId ?? + null, selectedOptionId, ); setImgDeleteModalOpen(false); diff --git a/src/components/mobile/organisms/BalanceGameEndingSection/BalanceGameEndingSection.tsx b/src/components/mobile/organisms/BalanceGameEndingSection/BalanceGameEndingSection.tsx index 1e6f1771..167c5d16 100644 --- a/src/components/mobile/organisms/BalanceGameEndingSection/BalanceGameEndingSection.tsx +++ b/src/components/mobile/organisms/BalanceGameEndingSection/BalanceGameEndingSection.tsx @@ -7,6 +7,8 @@ import { } from '@/assets'; import { PATH } from '@/constants/path'; import { useNavigate } from 'react-router-dom'; +import { useNewSelector } from '@/store'; +import { selectAccessToken } from '@/store/auth'; import { useGameEndBookmark } from '@/hooks/game/useBalanceGameBookmark'; import useToastModal from '@/hooks/modal/useToastModal'; import ToastModal from '@/components/atoms/ToastModal/ToastModal'; @@ -29,7 +31,7 @@ const BalanceGameEndingSection = ({ isMyEndBookmark, }: BalanceGameEndingSectionProps) => { const navigate = useNavigate(); - const isGuest = !localStorage.getItem('accessToken'); + const isGuest = !useNewSelector(selectAccessToken); const [shareModalOpen, setShareModalOpen] = useState(false); const { isVisible, modalText, showToastModal } = useToastModal(); diff --git a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts index 1f80c2bb..b53bae11 100644 --- a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts +++ b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts @@ -80,6 +80,7 @@ export const subTagWrapper = css({ display: 'flex', width: '100%', marginBottom: '50px', + gap: '8px', }); export const iconButtonWrapper = css({ diff --git a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.tsx b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.tsx index 9db5c040..01ceeadb 100644 --- a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.tsx +++ b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.tsx @@ -1,8 +1,12 @@ import React, { useState, useEffect, useRef } from 'react'; import { MobileBookmarkDF, MobileBookmarkPR, MobileShare } from '@/assets'; import { useNavigate } from 'react-router-dom'; +import { useNewSelector } from '@/store'; +import { selectAccessToken } from '@/store/auth'; import { GameDetail, GameSet } from '@/types/game'; +import { createArrayFromCommaString } from '@/utils/array'; import { PATH } from '@/constants/path'; +import { ERROR, PROMPT } from '@/constants/message'; import MenuTap, { MenuItem } from '@/components/atoms/MenuTap/MenuTap'; import useToastModal from '@/hooks/modal/useToastModal'; import { VoteRecord } from '@/types/vote'; @@ -15,7 +19,10 @@ import ToastModal from '@/components/atoms/ToastModal/ToastModal'; import BalanceGameBox from '@/components/mobile/molecules/BalanceGameBox/BalanceGameBox'; import { useGuestGameVote } from '@/hooks/game/useBalanceGameVote'; import { useGameBookmark } from '@/hooks/game/useBalanceGameBookmark'; -import ShareModal from '../../molecules/ShareModal/ShareModal'; +import { useDeleteGameSetMutation } from '@/hooks/api/game/useDeleteGameSetMutation'; +import ShareModal from '@/components/mobile/molecules/ShareModal/ShareModal'; +import TextModal from '@/components/mobile/molecules/TextModal/TextModal'; +import ReportModal from '@/components/mobile/molecules/ReportModal/ReportModal'; import * as S from './BalanceGameSection.style'; export interface BalanceGameSectionProps { @@ -51,11 +58,13 @@ const BalanceGameSection = ({ const gameStages: GameDetail[] = game?.gameDetailResponses ?? gameDefaultDetail; - const isGuest = !localStorage.getItem('accessToken'); + const isGuest = !useNewSelector(selectAccessToken); const [guestVotedList, setGuestVotedList] = useState([]); const currentGame: GameDetail = gameStages[currentStage]; + const subTagList = createArrayFromCommaString(game?.subTag ?? ''); + const { handleGuestGameVote } = useGuestGameVote( guestVotedList, setGuestVotedList, @@ -64,8 +73,16 @@ const BalanceGameSection = ({ game, ); - const [shareModalOpen, setShareModalOpen] = useState(false); const { isVisible, modalText, showToastModal } = useToastModal(); + const { mutate: deleteBalanceGame } = useDeleteGameSetMutation(); + + const [activeModal, setActiveModal] = useState< + 'reportGame' | 'reportText' | 'deleteText' | 'share' | 'none' + >('none'); + + const onCloseModal = () => { + setActiveModal('none'); + }; useEffect(() => { if (game && initialRender.current) { @@ -89,6 +106,20 @@ const BalanceGameSection = ({ changeStage(1); }; + const handleGameDeleteButton = () => { + deleteBalanceGame( + { gameSetId }, + { + onSuccess: () => { + navigate('/'); + }, + onError: () => { + showToastModal(ERROR.DELETEGAME.FAIL); + }, + }, + ); + }; + const { handleBookmarkClick } = useGameBookmark( isGuest, isMyGame, @@ -99,8 +130,28 @@ const BalanceGameSection = ({ game, ); - const myGameItem: MenuItem[] = [{ label: '수정' }, { label: '삭제' }]; - const otherGameItem: MenuItem[] = [{ label: '신고' }]; + const myGameItem: MenuItem[] = [ + { + label: '수정', + onClick: () => { + navigate(`/${PATH.CREATE.GAME}`, { state: { game, gameSetId } }); + }, + }, + { + label: '삭제', + onClick: () => { + setActiveModal('deleteText'); + }, + }, + ]; + const otherGameItem: MenuItem[] = [ + { + label: '신고', + onClick: () => { + setActiveModal('reportText'); + }, + }, + ]; return (
@@ -111,9 +162,26 @@ const BalanceGameSection = ({ )}
{}} + onClose={onCloseModal} + /> + + setActiveModal('reportGame')} + onClose={onCloseModal} + /> + {}} - onClose={() => setShareModalOpen(false)} + onClose={onCloseModal} />
@@ -121,7 +189,7 @@ const BalanceGameSection = ({
} - onClick={() => setShareModalOpen(true)} + onClick={() => setActiveModal('share')} />
- {game.subTag && } + {game.subTag && + subTagList.map((tag) => )}
)} diff --git a/src/components/molecules/BalanceGameEndingBox/BalanceGameEndingBox.tsx b/src/components/molecules/BalanceGameEndingBox/BalanceGameEndingBox.tsx index 1f4ee13d..8266603e 100644 --- a/src/components/molecules/BalanceGameEndingBox/BalanceGameEndingBox.tsx +++ b/src/components/molecules/BalanceGameEndingBox/BalanceGameEndingBox.tsx @@ -9,6 +9,8 @@ import ShareModal from '@/components/molecules/ShareModal/ShareModal'; import LoginModal from '@/components/molecules/LoginModal/LoginModal'; import useToastModal from '@/hooks/modal/useToastModal'; import { useGameEndBookmark } from '@/hooks/game/useBalanceGameBookmark'; +import { useNewSelector } from '@/store'; +import { selectAccessToken } from '@/store/auth'; import * as S from './BalanceGameEndingBox.style'; export interface BalanceGameEndingBoxProps { @@ -25,7 +27,7 @@ const BalanceGameEndingBox = ({ isMyEndBookmark, }: BalanceGameEndingBoxProps) => { const currentURL: string = window.location.href; - const isGuest = !localStorage.getItem('accessToken'); + const isGuest = !useNewSelector(selectAccessToken); const [loginModalOpen, setLoginModalOpen] = useState(false); const [shareModalOpen, setShareModalOpen] = useState(false); diff --git a/src/hooks/api/game/useCreateGameMutation.ts b/src/hooks/api/game/useCreateGameMutation.ts index fc5e92f6..e9ca164b 100644 --- a/src/hooks/api/game/useCreateGameMutation.ts +++ b/src/hooks/api/game/useCreateGameMutation.ts @@ -21,7 +21,7 @@ export const useCreateGameMutation = ( queryClient.invalidateQueries({ queryKey: ['games'], }); - showToastModal(SUCCESS.CREATEGAME.CREATE, () => { + showToastModal(SUCCESS.GAME.CREATE, () => { navigate(`/${PATH.BALANCEGAME.VIEW(gameId)}`); }); }, diff --git a/src/hooks/game/usePostBalanceGameForm.ts b/src/hooks/game/usePostBalanceGameForm.ts index a8a29c6e..594ba517 100644 --- a/src/hooks/game/usePostBalanceGameForm.ts +++ b/src/hooks/game/usePostBalanceGameForm.ts @@ -1,8 +1,12 @@ +import { PATH } from '@/constants/path'; import { useState } from 'react'; -import { BalanceGame, TempGame } from '@/types/game'; +import { useNavigate } from 'react-router-dom'; +import { UploadedImage } from '@/types/file'; +import { BalanceGame, GameSet, TempGame } from '@/types/game'; import { createInitialGameStages, transformBalanceGameToTempGame, + transformGameSetToBalanceGameSet, transformTempGameToBalanceGame, } from '@/utils/balanceGameUtils'; import { SUCCESS } from '@/constants/message'; @@ -13,18 +17,22 @@ import { useLoadTempGameQuery } from '@/hooks/api/game/useLoadTempGameQuery'; import { useSaveTempGameMutation } from '@/hooks/api/game/useSaveTempGameMutation'; import { useFileUploadMutation } from '@/hooks/api/file/useFileUploadMutation'; import { useDeleteFileMutation } from '@/hooks/api/file/useDeleteFileMutation'; -import { - validateBalanceGameForm, - validateGameTag, -} from './validateBalanceGameForm'; +import { validateBalanceGameForm } from './validateBalanceGameForm'; +import { useEditGamesMutation } from '../api/game/useEditGamesMutation'; export const usePostBalanceGameForm = ( gameStage: number, setGameStage: React.Dispatch>, setTagModalOpen: React.Dispatch>, + existingGame?: GameSet, + gameSetId?: number, ) => { + const navigate = useNavigate(); + const existingBalanceGame = + existingGame && transformGameSetToBalanceGameSet(existingGame); + const defaultGameOptions = createInitialGameStages(10); - const initialState: BalanceGame = { + const initialState: BalanceGame = existingBalanceGame ?? { title: '', mainTag: '', subTag: '', @@ -35,6 +43,7 @@ export const usePostBalanceGameForm = ( const { isVisible, modalText, showToastModal } = useToastModal(); const { mutate: createBalanceGame } = useCreateGameMutation(showToastModal); + const { mutate: editBalanceGame } = useEditGamesMutation(); const { mutate: uploadFiles } = useFileUploadMutation(); const { mutate: deleteFiles } = useDeleteFileMutation(); @@ -45,12 +54,23 @@ export const usePostBalanceGameForm = ( const [isTempGameLoaded, setIsTempGameLoaded] = useState(false); const handleBalanceGame = () => { - const gameValidation = validateGameTag(form); - - if (!gameValidation.isValid) { - return; + if (existingGame && gameSetId) { + editBalanceGame( + { + gameSetId, + data: form, + }, + { + onSuccess: () => { + showToastModal(SUCCESS.GAME.EDIT, () => { + navigate(`/${PATH.BALANCEGAME.VIEW(gameSetId)}`); + }); + }, + }, + ); + } else { + createBalanceGame(form); } - createBalanceGame(form); }; const handleTempBalanceGame = () => { @@ -91,7 +111,7 @@ export const usePostBalanceGameForm = ( params: { type: 'GAME_OPTION' }, }, { - onSuccess: (res) => { + onSuccess: (res: UploadedImage) => { setEach('fileId', res.fileIds[0], gameStage, optionId); setEach('imgUrl', res.imgUrls[0], gameStage, optionId); }, @@ -101,7 +121,12 @@ export const usePostBalanceGameForm = ( }; const handleDeleteImg = (fileId: number | null, optionId: number) => { - if (fileId) { + if (!fileId) return; + + if (existingBalanceGame || isTempGameLoaded) { + setEach('fileId', null, gameStage, optionId); + setEach('imgUrl', '', gameStage, optionId); + } else { deleteFiles(fileId, { onSuccess: () => { setEach('fileId', null, gameStage, optionId); diff --git a/src/hooks/game/validateBalanceGameForm.ts b/src/hooks/game/validateBalanceGameForm.ts index 4408bd23..e472671a 100644 --- a/src/hooks/game/validateBalanceGameForm.ts +++ b/src/hooks/game/validateBalanceGameForm.ts @@ -1,5 +1,6 @@ import { ERROR } from '@/constants/message'; -import { isEmptyString } from '@/utils/validator'; +import { isAllLessThan, isEmptyString } from '@/utils/validator'; +import { createArrayFromCommaString } from '@/utils/array'; import { BalanceGame } from '@/types/game'; export const validateBalanceGameForm = ( @@ -24,9 +25,12 @@ export const validateBalanceGameForm = ( }; export const validateGameTag = (form: BalanceGame) => { - if (isEmptyString(form.mainTag)) { - return { isValid: false }; - } + const subTagList = createArrayFromCommaString(form.subTag); + + const isValid = + !isEmptyString(form.mainTag) && + subTagList.length <= 3 && + isAllLessThan(subTagList, 10); - return { isValid: true }; + return { isValid }; }; diff --git a/src/pages/mobile/BalanceGameCreationMobilePage/BalanceGameCreationMobilePage.tsx b/src/pages/mobile/BalanceGameCreationMobilePage/BalanceGameCreationMobilePage.tsx index 33b41501..4eae88ae 100644 --- a/src/pages/mobile/BalanceGameCreationMobilePage/BalanceGameCreationMobilePage.tsx +++ b/src/pages/mobile/BalanceGameCreationMobilePage/BalanceGameCreationMobilePage.tsx @@ -1,11 +1,27 @@ import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { GameSet } from '@/types/game'; import BalanceGameCreateSection from '@/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection'; import * as S from './BalanceGameCreationMobilePage.style'; +interface State { + game: GameSet; + gameSetId: number; +} + const BalanceGameCreationMobilePage = () => { + const location = useLocation(); + const state = location.state as State; + + const balanceGameData = state?.game; + const balanceGameSetId = state?.gameSetId; + return (
- +
); }; diff --git a/src/pages/mobile/BalanceGameMobilePage/BalanceGameMobilePage.tsx b/src/pages/mobile/BalanceGameMobilePage/BalanceGameMobilePage.tsx index d0ee6ad4..d255a73c 100644 --- a/src/pages/mobile/BalanceGameMobilePage/BalanceGameMobilePage.tsx +++ b/src/pages/mobile/BalanceGameMobilePage/BalanceGameMobilePage.tsx @@ -1,8 +1,5 @@ import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useNewSelector } from '@/store'; -import { selectAccessToken } from '@/store/auth'; -import { useParseJwt } from '@/hooks/common/useParseJwt'; import { useMemberQuery } from '@/hooks/api/member/useMemberQuery'; import { useGameBySetId } from '@/hooks/api/game/useGameBySetIdQuery'; import BalanceGameSection from '@/components/mobile/organisms/BalanceGameSection/BalanceGameSection'; @@ -10,15 +7,14 @@ import BalanceGameEndingSection from '@/components/mobile/organisms/BalanceGameE import * as S from './BalanceGameMobilePage.style'; const BalanceGameMobilePage = () => { + const { member } = useMemberQuery(); + const { setId } = useParams<{ setId: string }>(); const gameSetId = Number(setId); const { gameSet } = useGameBySetId(gameSetId); const [currentStage, setCurrentStage] = useState(0); - const accessToken = useNewSelector(selectAccessToken); - const { member } = useMemberQuery(useParseJwt(accessToken).memberId); - const isMyGame: boolean = member?.nickname === gameSet?.member; const changeStage = (step: number) => { diff --git a/src/utils/array.ts b/src/utils/array.ts index 96fbf7a1..cec126f5 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -9,3 +9,7 @@ export const createRangeArray = (currentPage: number, maxPage: number) => { (_, i) => startPage + i, ); }; + +export const createArrayFromCommaString = (str: string): string[] => { + return str.split(','); +}; diff --git a/src/utils/balanceGameUtils.ts b/src/utils/balanceGameUtils.ts index 58dc7829..6139f7c5 100644 --- a/src/utils/balanceGameUtils.ts +++ b/src/utils/balanceGameUtils.ts @@ -109,3 +109,24 @@ export const transformTempGameToBalanceGame = ( })), })), }); + +export const transformGameSetToBalanceGameSet = ( + gameSet: GameSet, +): BalanceGame => { + return { + title: gameSet.title, + mainTag: gameSet.mainTag, + subTag: gameSet.subTag, + games: gameSet.gameDetailResponses.map((gameDetail) => ({ + description: gameDetail.description ?? '', + gameOptions: gameDetail.gameOptions.map((option) => ({ + id: option.id, + name: option.name, + imgUrl: option.imgUrl ?? '', + description: option.description, + optionType: option.optionType, + fileId: option.fileId ?? null, + })), + })), + }; +}; diff --git a/src/utils/validator.ts b/src/utils/validator.ts index f0dc6d02..19caa245 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -18,4 +18,8 @@ const isTimeLimit = (timeLimit: number) => { return differenceTime < 2; }; -export { isEmptyString, isLongerThan, isAllTrue, isTimeLimit }; +const isAllLessThan = (arr: string[], num: number) => { + return arr.every((str) => str.length <= num); +}; + +export { isEmptyString, isLongerThan, isAllTrue, isTimeLimit, isAllLessThan };