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/파티 패널티시 방 참여 및 생성 제한 #1341 #1345

Merged
20 changes: 19 additions & 1 deletion components/Layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useContext } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { BsMegaphone } from 'react-icons/bs';
import { FaArrowLeft } from 'react-icons/fa';
import { FiMenu } from 'react-icons/fi';
import { IoStorefrontOutline } from 'react-icons/io5';
import { Modal } from 'types/modalTypes';
Expand All @@ -18,6 +20,7 @@ import useAxiosGet from 'hooks/useAxiosGet';
import styles from 'styles/Layout/Header.module.scss';

export default function Header() {
const router = useRouter();
const [live, setLive] = useRecoilState(liveState);
const HeaderState = useContext<HeaderContextState | null>(HeaderContext);
const openMenuBarHandler = () => {
Expand Down Expand Up @@ -54,11 +57,26 @@ export default function Header() {
type: 'setError',
});

// 현재 경로가 뒤로 가기 버튼을 사용해야 하는 경로 패턴 중 하나와 일치하는지 확인
const isBackButtonRoute = () => {
const path = router.asPath.split('?')[0]; // 쿼리 스트링 제거
const patterns = [
/^\/party\/create$/, // '/party/create'
/^\/party\/[0-9]+$/, // '/party/[roomId]'
];

return patterns.some((pattern) => pattern.test(path));
};

return (
<div className={styles.headerContainer}>
<div className={styles.headerWrap}>
<div className={styles.headerLeft}>
<FiMenu className={styles.menuIcon} onClick={openMenuBarHandler} />
{isBackButtonRoute() ? (
<FaArrowLeft onClick={router.back} />
) : (
<FiMenu className={styles.menuIcon} onClick={openMenuBarHandler} />
)}
</div>
<Link className={styles.logoWrap} href={'/'}>
42GG
Expand Down
2 changes: 1 addition & 1 deletion components/admin/party/PartyCategory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PartyCategory } from 'types/partyTypes';
import { tableFormat } from 'constants/admin/table';
import { AdminTableHead } from 'components/admin/common/AdminTable';
import usePartyCategory from 'hooks/party/usePartyCategory';
import styles from 'styles/admin/Party/AdminPartyCommon.module.scss';
import styles from 'styles/admin/party/AdminPartyCommon.module.scss';

const tableTitle: { [key: string]: string } = {
categoryId: '카테고리번호',
Expand Down
24 changes: 19 additions & 5 deletions components/modal/Party/PartyRoomEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,24 @@ export default function PartyRoomEditModal({ roomId }: { roomId: number }) {
const [room, setRoom] = useState<PartyRoomDetail>();

function handleStatus(e: ChangeEvent<HTMLSelectElement>) {
setRoom({
...(room as PartyRoomDetail),
status: e.target.value as PartyRoomStatus,
});
instanceInPartyManage
.patch(`/rooms/${room?.roomId}`, {
status: e.target.value,
})
.then(() => {
setRoom({
...(room as PartyRoomDetail),
status: e.target.value as PartyRoomStatus,
});
})
.catch(() => {
setSnackBar({
toastName: 'PATCH request',
message: '방 상태를 변경할 수 없습니다',
severity: 'error',
clicked: true,
});
});
}
function handleCommentHidden(comment: PartyComment) {
instanceInPartyManage
Expand Down Expand Up @@ -65,7 +79,7 @@ export default function PartyRoomEditModal({ roomId }: { roomId: number }) {
<span className={styles.categoryName}>#{room.categoryName}</span>
{room.title}
</h2>
<select onChange={handleStatus}>
<select onChange={handleStatus} defaultValue={room.status}>
{roomStatusOpts.map((opt) => (
<option key={opt} value={opt}>
{opt}
Expand Down
2 changes: 1 addition & 1 deletion components/modal/admin/AdminTemplateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function TemplateModal({
const [formData, setFormData] = useState<PartyTemplateForm>(
template ?? {
gameName: '',
categoryId: 1,
categoryName: '기타',
maxGamePeople: 1,
minGamePeople: 1,
maxGameTime: 1,
Expand Down
2 changes: 1 addition & 1 deletion components/modal/modalType/AdminModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import DetailModal from 'components/modal/admin/DetailModal';
import AdminSeasonEdit from 'components/modal/admin/SeasonEdit';
import AdminEditTournamentBraket from '../admin/AdminEditTournamentBraket';
import AdminTournamentParticipantEditModal from '../admin/AdminTournamentParticipantEditModal/AdminTournamentParticipantEditModal';
import PartyRoomEditModal from '../Party/PartyRoomEditModal';
import PartyRoomEditModal from '../party/PartyRoomEditModal';

export default function AdminModal() {
const {
Expand Down
2 changes: 1 addition & 1 deletion components/modal/modalType/PartyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';
import { modalState } from 'utils/recoil/modal';
import { PartyReportModal } from '../Party/PartyReportModal';
import { PartyReportModal } from '../party/PartyReportModal';

export default function PartyModal() {
const { modalName, partyReport } = useRecoilValue(modalState);
Expand Down
136 changes: 136 additions & 0 deletions components/party/PartyMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { FaSearch } from 'react-icons/fa';
import { PartyCategory, PartyRoom } from 'types/partyTypes';
import styles from 'styles/party/PartyMain.module.scss';
import PartyRoomListItem from './PartyRoomListItem';

// ================================================================================
// JoinedRooms : 참여한 방 리스트 및 방 생성 버튼
// ================================================================================

type JoinedRoomsProps = {
joinedPartyRooms: PartyRoom[];
penaltyPeroid: string | null;
};

function JoinedRooms({ joinedPartyRooms, penaltyPeroid }: JoinedRoomsProps) {
return (
<section className={styles.joinedRoomContainer}>
<header className={styles.joinedRoomHeader}>
<h2>참여중인 파티</h2>
{penaltyPeroid ? (
<div className={styles.penalty}>
패널티 <span className={styles.timer}>{penaltyPeroid}</span>
</div>
) : (
<Link href='/party/create' className={styles.createRoomButton}>
방 만들기
</Link>
)}
</header>
<ul>
{joinedPartyRooms.length > 0 ? (
joinedPartyRooms.map((room) => (
<PartyRoomListItem key={room.roomId} room={room} />
))
) : (
<>참여중인 방이 없습니다.</>
)}
</ul>
</section>
);
}

// ================================================================================
// SearchBar: 검색창
// ================================================================================

function SearchBar({ titleQuery = '' }: { titleQuery?: string }) {
const router = useRouter();
const [searchTitle, setSearchTitle] = useState(titleQuery);

return (
<section className={styles.searchBar}>
<form
onSubmit={(e) => {
e.preventDefault();
router.replace({
pathname: router.pathname,
query: { title: searchTitle },
});
}}
>
<FaSearch className={styles.searchIcon} />
<input
placeholder='방 검색하기'
defaultValue={searchTitle}
onChange={(e) => {
setSearchTitle(e.target.value);
}}
/>
</form>
</section>
);
}

// ================================================================================
// AllRooms: 모든 방 리스트
// ================================================================================

const noFilter: PartyCategory = {
categoryId: 0,
categoryName: '전체',
};

type AllRoomsProps = {
partyRooms: PartyRoom[];
isSearchedResult: boolean;
categories: PartyCategory[];
};

function AllRooms({ partyRooms, isSearchedResult, categories }: AllRoomsProps) {
const [categoryFilter, setCategoryFilter] = useState(noFilter.categoryName);
const filteredRooms = partyRooms.filter(
(room) =>
categoryFilter === noFilter.categoryName ||
categoryFilter === room.categoryName
);
const categoryNavItems = [noFilter, ...categories];

return (
<section className={styles.allRoomContanier}>
<nav className={styles.categoryNav}>
<ul>
{categoryNavItems.map((c) => (
<li
key={c.categoryName}
onClick={() => setCategoryFilter(c.categoryName)}
className={
categoryFilter === c.categoryName ? styles.selected : ''
}
>
{c.categoryName}
</li>
))}
</ul>
</nav>
<div className={styles.allRoomListWrap}>
{filteredRooms.length > 0 ? (
<ul>
{filteredRooms.map((room) => (
<PartyRoomListItem key={room.roomId} room={room} />
))}
</ul>
) : isSearchedResult ? (
<div className={styles.emptyRooms}>검색결과가 없습니다.</div>
) : (
<div className={styles.emptyRooms}>모집중인 방이 없습니다.</div>
)}
</div>
</section>
);
}

export const PartyMain = { JoinedRooms, SearchBar, AllRooms };
13 changes: 12 additions & 1 deletion components/party/roomDetail/PartyDetailButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LuAlertTriangle } from 'react-icons/lu';
import { instance } from 'utils/axios';
import { modalState } from 'utils/recoil/modal';
import { toastState } from 'utils/recoil/toast';
import usePartyPenaltyTimer from 'hooks/party/usePartyPenaltyTimer';
import styles from 'styles/party/PartyDetailRoom.module.scss';

type ParytButtonProps = {
Expand Down Expand Up @@ -108,6 +109,8 @@ type RefreshProps = ParytButtonProps & {

function JoinRoom({ roomId, fetchRoomDetail }: RefreshProps) {
const setSnackbar = useSetRecoilState(toastState);
const { penaltyPeroid } = usePartyPenaltyTimer();

const handlerJoin = () => {
instance
.post(`/party/rooms/${roomId}`)
Expand All @@ -124,7 +127,15 @@ function JoinRoom({ roomId, fetchRoomDetail }: RefreshProps) {
});
};

return (
return penaltyPeroid ? (
<button
className={`${styles.joinBtn} ${styles.penalty}`}
onClick={handlerJoin}
>
패널티 부여 중<br />
{penaltyPeroid}
</button>
) : (
<button className={styles.joinBtn} onClick={handlerJoin}>
참여하기
</button>
Expand Down
8 changes: 0 additions & 8 deletions components/party/roomDetail/PartyDetailProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Image from 'next/image';
import { useRouter } from 'next/router';
import { FaCrown } from 'react-icons/fa';
import {
PartyRoomDetail,
Expand All @@ -21,18 +20,11 @@ export default function PartyDetailProfile({
}: PartyDetailProfileProps) {
const { currentPeople, minPeople, roomId, status, roomUsers, hostNickname } =
partyRoomDetail;
const router = useRouter();

return (
<div className={styles.profile}>
<div className={styles.line}>
<span>{`인원 : ${currentPeople}`}</span>
<button
className={styles.exitBtn}
onClick={() => router.push('/party')}
>
로비
</button>
</div>
<div className={styles.profileItem}>
<Profile
Expand Down
1 change: 1 addition & 0 deletions hooks/party/usePartyCategory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function usePartyCategory() {
clicked: true,
});
},
staleTime: Infinity,
});

const createMutation = useMutation(
Expand Down
57 changes: 57 additions & 0 deletions hooks/party/usePartyPenaltyTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { instance } from 'utils/axios';
import { calculatePeriod } from 'utils/handleTime';

type PartyPenaltyRes = {
penaltyEndTime: string | null;
};

export default function usePartyPenaltyTimer() {
const { data, isLoading: isQueryLoading } = useQuery({
queryKey: 'partyPenalty',
queryFn: () =>
instance.get<PartyPenaltyRes>('/party/penalty').then(({ data }) => ({
...data,
penaltyEndTime:
data.penaltyEndTime && data.penaltyEndTime
? new Date(data.penaltyEndTime)
: null,
})),
staleTime: 5 * 60 * 1000, // 5분
});
const partyPenalty = data ?? { penaltyEndTime: null };
const [penaltyPeroid, setPenaltyPeroid] = useState(
partyPenalty.penaltyEndTime && calculatePeriod(partyPenalty.penaltyEndTime)
);
const [isPenaltyLoading, setIsPenaltyLoading] = useState(true);

useEffect(() => {
let intervalId: ReturnType<typeof setInterval>;

if (!isQueryLoading) {
if (
partyPenalty.penaltyEndTime &&
partyPenalty.penaltyEndTime.getTime() > Date.now()
) {
setIsPenaltyLoading(false);

setPenaltyPeroid(calculatePeriod(partyPenalty.penaltyEndTime));
intervalId = setInterval(() => {
if (partyPenalty.penaltyEndTime!.getTime() > Date.now())
setPenaltyPeroid(calculatePeriod(partyPenalty.penaltyEndTime!));
else {
setPenaltyPeroid(null);
clearInterval(intervalId);
}
}, 1000);
} else {
setIsPenaltyLoading(false);
}
}

return () => clearInterval(intervalId);
}, [isQueryLoading, partyPenalty.penaltyEndTime]);

return { penaltyPeroid, isPenaltyLoading };
}
Loading