diff --git a/src/features/sessions/api/index.ts b/src/features/sessions/api/index.ts index b3186e3..d1d55ec 100644 --- a/src/features/sessions/api/index.ts +++ b/src/features/sessions/api/index.ts @@ -49,3 +49,11 @@ export async function endSession(sessionId: string): Promise ); return data; } + +/** 참가자 강퇴 (호스트) */ +export async function deleteParticipant( + sessionId: string, + participantId: string, +): Promise { + await apiClient.delete(`/sessions/${sessionId}/participants/${participantId}`); +} diff --git a/src/features/sessions/hooks/index.ts b/src/features/sessions/hooks/index.ts index 5fd540d..481a95d 100644 --- a/src/features/sessions/hooks/index.ts +++ b/src/features/sessions/hooks/index.ts @@ -3,3 +3,4 @@ export { useCreateSession } from './useCreateSession'; export { useStartSession } from './useStartSession'; export { useNextQuestion } from './useNextQuestion'; export { useEndSession } from './useEndSession'; +export { useKickParticipant } from './useKickParticipant'; diff --git a/src/features/sessions/hooks/useKickParticipant.ts b/src/features/sessions/hooks/useKickParticipant.ts new file mode 100644 index 0000000..fc45e10 --- /dev/null +++ b/src/features/sessions/hooks/useKickParticipant.ts @@ -0,0 +1,16 @@ +'use client'; + +import { useMutation } from '@tanstack/react-query'; +import { deleteParticipant } from '../api'; + +/** + * 참가자 강퇴 (호스트) + * + * 성공 시 서버가 /topic/.../participants 로 갱신된 목록을 브로드캐스트하므로 + * 별도 invalidate 없이 소켓 이벤트로 목록이 갱신됩니다. + */ +export function useKickParticipant(sessionId: string) { + return useMutation({ + mutationFn: (participantId: string) => deleteParticipant(sessionId, participantId), + }); +} diff --git a/src/features/sessions/ui/HostWaitingClient.tsx b/src/features/sessions/ui/HostWaitingClient.tsx index 6c6582d..785a281 100644 --- a/src/features/sessions/ui/HostWaitingClient.tsx +++ b/src/features/sessions/ui/HostWaitingClient.tsx @@ -2,26 +2,26 @@ import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { useSession, useStartSession } from '../hooks'; +import { useSession, useStartSession, useKickParticipant } from '../hooks'; import { useSessionSocket } from '../socket/hooks'; import { getApiErrorMessage } from '@/shared/api/error'; import { CountUp } from '@/shared/ui/CountUp'; +import type { SessionParticipantsEvent } from '@/shared/types/api'; export function HostWaitingClient({ sessionId }: { sessionId: string }) { const router = useRouter(); const { data: session, isLoading } = useSession(sessionId); const { mutate: startSession, isPending } = useStartSession(); + const { mutate: kick, isPending: kicking } = useKickParticipant(sessionId); const [errorMsg, setErrorMsg] = useState(''); + const [participants, setParticipants] = useState([]); - // WebSocket — 참가자 입장 이벤트 수신 + // WebSocket — 참가자 입·퇴장 실시간 목록 const { connected } = useSessionSocket({ sessionId, enabled: !!session, - onParticipants: () => { - // 참가자 목록 변경 시 session 쿼리 자동 refetch (3초 폴링) - }, + onParticipants: (e) => setParticipants(e.participants), onStatus: (e) => { - // 세션 상태 변경 감지 if (e.status === 'IN_PROGRESS') { router.push(`/host/sessions/${sessionId}/play`); } @@ -52,7 +52,9 @@ export function HostWaitingClient({ sessionId }: { sessionId: string }) { ); } - const canStart = !isPending && session.participantCount > 0; + // 실시간 인원: WS 목록이 있으면 그 길이, 없으면 폴링 카운트 + const liveCount = participants.length > 0 ? participants.length : session.participantCount; + const canStart = !isPending && liveCount > 0; return (
@@ -66,7 +68,7 @@ export function HostWaitingClient({ sessionId }: { sessionId: string }) {
- {/* PIN 디스플레이 — RED 네온 */} + {/* PIN — RED 네온 */}
- + 명 참가 중 @@ -98,6 +100,37 @@ export function HostWaitingClient({ sessionId }: { sessionId: string }) { )}
+ {/* 참가자 목록 + 강퇴 */} + {participants.length > 0 && ( +
+

+ 참가자 목록 +

+
+ {participants.map((p) => ( +
+ + {p.nickname} + + +
+ ))} +
+
+ )} + {/* 세션 정보 */}
@@ -130,7 +163,7 @@ export function HostWaitingClient({ sessionId }: { sessionId: string }) { {isPending ? '시작 중…' : '퀴즈 시작!'} - {session.participantCount === 0 && ( + {liveCount === 0 && (

참가자가 1명 이상 있어야 시작할 수 있습니다.