From dc00930a884cb8908a6f0390758809a0427d3f95 Mon Sep 17 00:00:00 2001 From: lsmin3388 Date: Mon, 8 Jun 2026 15:38:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(host):=20=EB=8C=80=EA=B8=B0=EC=8B=A4=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=B0=B8=EA=B0=80=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20+=20=EA=B0=95=ED=87=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HostWaitingClient: WS participants 이벤트로 실시간 참가자 목록 표시 + 각 참가자 강퇴 버튼, 인원 카운트도 목록 기준 실시간 반영 - useKickParticipant 훅 + deleteParticipant API(DELETE /sessions/{id}/participants/{pid}) — 백엔드 강퇴 API와 연동 - 강퇴 성공 시 서버 participants 브로드캐스트로 목록 자동 갱신 --- src/features/sessions/api/index.ts | 8 +++ src/features/sessions/hooks/index.ts | 1 + .../sessions/hooks/useKickParticipant.ts | 16 ++++++ .../sessions/ui/HostWaitingClient.tsx | 53 +++++++++++++++---- 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/features/sessions/hooks/useKickParticipant.ts 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명 이상 있어야 시작할 수 있습니다.