diff --git a/client/src/components/canvas/CanvasUI.tsx b/client/src/components/canvas/CanvasUI.tsx
index 12ed766b5..7f60e6481 100644
--- a/client/src/components/canvas/CanvasUI.tsx
+++ b/client/src/components/canvas/CanvasUI.tsx
@@ -95,7 +95,6 @@ interface CanvasProps extends HTMLAttributes {
inkRemaining: number;
maxPixels: number;
canvasEvents: CanvasEventHandlers;
- isHidden: boolean;
showInkRemaining: boolean;
}
@@ -119,7 +118,6 @@ const Canvas = forwardRef(
inkRemaining,
maxPixels,
canvasEvents,
- isHidden,
showInkRemaining,
...props
},
@@ -131,7 +129,6 @@ const Canvas = forwardRef(
className={cn(
'relative flex w-full max-w-screen-sm flex-col border-violet-500 bg-white',
'sm:rounded-lg sm:border-4 sm:shadow-xl',
- isHidden && 'hidden',
className,
)}
{...props}
diff --git a/client/src/components/canvas/GameCanvas.tsx b/client/src/components/canvas/GameCanvas.tsx
index 541ef6b1b..53092032f 100644
--- a/client/src/components/canvas/GameCanvas.tsx
+++ b/client/src/components/canvas/GameCanvas.tsx
@@ -1,5 +1,5 @@
-import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef } from 'react';
-import { PlayerRole, RoomStatus } from '@troublepainter/core';
+import { PointerEvent, useCallback, useEffect, useRef } from 'react';
+import { PlayerRole } from '@troublepainter/core';
import { Canvas } from '@/components/canvas/CanvasUI';
import { COLORS_INFO, DEFAULT_MAX_PIXELS, MAINCANVAS_RESOLUTION_WIDTH } from '@/constants/canvasConstants';
import { handleInCanvas, handleOutCanvas } from '@/handlers/canvas/cursorInOutHandler';
@@ -13,12 +13,10 @@ import { getCanvasContext } from '@/utils/getCanvasContext';
import { getDrawPoint } from '@/utils/getDrawPoint';
interface GameCanvasProps {
- isHost: boolean;
role: PlayerRole;
maxPixels?: number;
currentRound: number;
- roomStatus: RoomStatus;
- isHidden: boolean;
+ isDrawable: boolean;
}
/**
@@ -50,7 +48,7 @@ interface GameCanvasProps {
*
* @category Components
*/
-const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomStatus, isHidden }: GameCanvasProps) => {
+const GameCanvas = ({ maxPixels = DEFAULT_MAX_PIXELS, currentRound, isDrawable }: GameCanvasProps) => {
const canvasRef = useRef(null);
const cursorCanvasRef = useRef(null);
const { convertCoordinate } = useCoordinateScale(MAINCANVAS_RESOLUTION_WIDTH, canvasRef);
@@ -73,7 +71,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
redo,
makeCRDTSyncMessage,
resetCanvas,
- } = useDrawing(canvasRef, roomStatus, {
+ } = useDrawing(canvasRef, isDrawable, {
maxPixels,
});
@@ -120,7 +118,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
}));
const handleDrawStart = useCallback(
- (e: ReactMouseEvent | ReactTouchEvent) => {
+ (e: PointerEvent) => {
if (!isConnected) return;
const { canvas } = getCanvasContext(canvasRef);
@@ -136,7 +134,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
);
const handleDrawMove = useCallback(
- (e: ReactMouseEvent | ReactTouchEvent) => {
+ (e: PointerEvent) => {
const { canvas } = getCanvasContext(canvasRef);
const point = getDrawPoint(e, canvas);
const convertPoint = convertCoordinate(point);
@@ -152,7 +150,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
);
const handleDrawLeave = useCallback(
- (e: ReactMouseEvent | ReactTouchEvent) => {
+ (e: PointerEvent) => {
const { canvas } = getCanvasContext(canvasRef);
const point = getDrawPoint(e, canvas);
const convertPoint = convertCoordinate(point);
@@ -202,8 +200,7 @@ const GameCanvas = ({ role, maxPixels = DEFAULT_MAX_PIXELS, currentRound, roomSt
{messages.map((message) => {
- const isOthers = message.playerId !== currentPlayerId;
+ const isOthers = message.playerId !== playerId;
return (
{
- const { isHost, buttonConfig, handleStartGame, isStarting } = useGameStart();
+ const { startGame, checkCanStart, getStartButtonStatus, isStarting } = useGameStart();
+ const { disabled, title, content } = getStartButtonStatus();
+
+ useShortcuts([
+ {
+ key: 'GAME_START',
+ action: () => startGame(),
+ },
+ ]);
+
return (
);
};
diff --git a/client/src/components/modal/RoleModal.tsx b/client/src/components/modal/RoleModal.tsx
index 0518ab794..8ce4d8002 100644
--- a/client/src/components/modal/RoleModal.tsx
+++ b/client/src/components/modal/RoleModal.tsx
@@ -1,17 +1,11 @@
-import { useEffect } from 'react';
import { Modal } from '@/components/ui/Modal';
import { PLAYING_ROLE_TEXT } from '@/constants/gameConstant';
-import { useModal } from '@/hooks/useModal';
+import { useRoleModal } from '@/hooks/game/useRoleModal';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
const RoleModal = () => {
- const room = useGameSocketStore((state) => state.room);
+ const { isModalOpened, closeModal, handleKeyDown } = useRoleModal();
const roundAssignedRole = useGameSocketStore((state) => state.roundAssignedRole);
- const { isModalOpened, closeModal, handleKeyDown, openModal } = useModal(5000);
-
- useEffect(() => {
- if (roundAssignedRole) openModal();
- }, [roundAssignedRole, room?.currentRound]);
return (
{
- const room = useGameSocketStore((state) => state.room);
- const roundWinners = useGameSocketStore((state) => state.roundWinners);
- const players = useGameSocketStore((state) => state.players);
- const currentPlayerId = useGameSocketStore((state) => state.currentPlayerId);
- const timer = useTimerStore((state) => state.timers.ENDING);
-
- const { isModalOpened, openModal, closeModal } = useModal();
- const [showAnimation, setShowAnimation] = useState(false);
- const [isAnimationFading, setIsAnimationFading] = useState(false);
-
- const devil = players.find((player) => player.role === PlayerRole.DEVIL);
- const isDevilWin = roundWinners?.some((winner) => winner.role === PlayerRole.DEVIL);
- const isCurrentPlayerWinner = roundWinners?.some((winner) => winner.playerId === currentPlayerId);
-
- // 컴포넌트 마운트 시 사운드 미리 로드
- const soundManager = SoundManager.getInstance();
- useEffect(() => {
- soundManager.preloadSound(SOUND_IDS.WIN, gameWin);
- soundManager.preloadSound(SOUND_IDS.LOSS, gameLoss);
- }, [soundManager]);
-
- useEffect(() => {
- if (roundWinners) {
- setIsAnimationFading(false);
- setShowAnimation(true);
- openModal();
-
- if (isCurrentPlayerWinner) {
- void soundManager.playSound(SOUND_IDS.WIN, 0.3);
- } else {
- void soundManager.playSound(SOUND_IDS.LOSS, 0.3);
- }
- }
- }, [roundWinners]);
-
- useEffect(() => {
- if (room && room.status === RoomStatus.DRAWING) closeModal();
- }, [room]);
-
- useEffect(() => {
- if (showAnimation) {
- // 3초 후에 페이드아웃 시작
- const fadeTimer = setTimeout(() => {
- setIsAnimationFading(true);
- }, 3000);
-
- // 3.5초 후에 컴포넌트 제거
- const removeTimer = setTimeout(() => {
- setShowAnimation(false);
- }, 3500);
-
- return () => {
- clearTimeout(fadeTimer);
- clearTimeout(removeTimer);
- };
- }
- }, [showAnimation]);
+ const { showAnimation, isPlayerWinner, isAnimationFading, isModalOpened, timer, isDevilWin, solver, devil } =
+ useRoundEndModal();
+ const currentWord = useGameSocketStore((state) => state.room?.currentWord);
return (
<>
{/* 승리/패배 애니메이션 */}
{showAnimation &&
- (isCurrentPlayerWinner ? (
+ (isPlayerWinner ? (
{
/>
))}
-
+
{timer}
@@ -111,10 +47,7 @@ const RoundEndModal = () => {
<> 정답을 맞춘 구경꾼이 없습니다>
) : (
<>
- 구경꾼{' '}
-
- {roundWinners?.find((winner) => winner.role === PlayerRole.GUESSER)?.nickname}
-
+ 구경꾼 {solver?.nickname}
이(가) 정답을 맞혔습니다
>
)}
diff --git a/client/src/components/player/PlayerCardList.tsx b/client/src/components/player/PlayerCardList.tsx
index 16a017ad9..2b79f71ba 100644
--- a/client/src/components/player/PlayerCardList.tsx
+++ b/client/src/components/player/PlayerCardList.tsx
@@ -1,26 +1,22 @@
import { memo } from 'react';
-import { PlayerRole, PlayerStatus } from '@troublepainter/core';
+import { PlayerStatus } from '@troublepainter/core';
import { PlayerCard } from '@/components/ui/player-card/PlayerCard';
+import { usePlayers } from '@/hooks/game/usePlayers';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
const PlayerCardList = memo(() => {
- // 개별 selector 사용으로 변경
- const players = useGameSocketStore((state) => state.players);
+ // roomSettings 제외하고 필요한 것만 구독
const hostId = useGameSocketStore((state) => state.room?.hostId);
- const roundAssignedRole = useGameSocketStore((state) => state.roundAssignedRole);
- const currentPlayerId = useGameSocketStore((state) => state.currentPlayerId);
+ const players = useGameSocketStore((state) => state.players);
+ const playerId = useGameSocketStore((state) => state.currentPlayerId);
+ const { getDisplayRoleText } = usePlayers();
if (!players?.length) return null;
- const getPlayerRole = (playerRole: PlayerRole | undefined, myRole: PlayerRole | null) => {
- if (myRole === PlayerRole.GUESSER) return playerRole === PlayerRole.GUESSER ? playerRole : null;
- return playerRole;
- };
-
return (
<>
{players.map((player) => {
- const playerRole = getPlayerRole(player.role, roundAssignedRole) || null;
+ const playerRole = getDisplayRoleText(player.role) || null;
return (
{
role={playerRole}
score={player.score}
isHost={player.status === PlayerStatus.NOT_PLAYING && player.playerId === hostId} // 이 플레이어가 방장인지
- isMe={player.playerId === currentPlayerId}
+ isMe={player.playerId === playerId}
/>
);
})}
diff --git a/client/src/components/quiz/QuizStage.tsx b/client/src/components/quiz/QuizStage.tsx
index a2a60f92d..dc40b68af 100644
--- a/client/src/components/quiz/QuizStage.tsx
+++ b/client/src/components/quiz/QuizStage.tsx
@@ -1,93 +1,51 @@
-import { useMemo } from 'react';
-import { PlayerRole, RoomStatus } from '@troublepainter/core';
+import { PlayerRole } from '@troublepainter/core';
import { GameCanvas } from '../canvas/GameCanvas';
import { QuizTitle } from '../ui/QuizTitle';
import sizzlingTimer from '@/assets/big-timer.gif';
import { DEFAULT_MAX_PIXELS } from '@/constants/canvasConstants';
-import { useTimer } from '@/hooks/useTimer';
+import { useQuizStageUI } from '@/hooks/game/useQuizStageUI';
+import { useTimer } from '@/hooks/game/useTimer';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
-import { cn } from '@/utils/cn';
const QuizStageContainer = () => {
- const room = useGameSocketStore((state) => state.room);
+ const { checkShowBigTimer, checkShowCanvasAndQuizTitle, getQuizTitleText, checkCanCanvasDraw } = useQuizStageUI();
+ const { getRemainingTime } = useTimer();
const roomSettings = useGameSocketStore((state) => state.roomSettings);
+ const room = useGameSocketStore((state) => state.room);
const roundAssignedRole = useGameSocketStore((state) => state.roundAssignedRole);
- const isHost = useGameSocketStore((state) => state.isHost);
-
- if (!room || !roomSettings || isHost === null) return null;
-
- const timers = useTimer();
-
- const shouldHideCanvas = useMemo(() => {
- const isGuesser = roundAssignedRole === PlayerRole.GUESSER;
- const isDrawing = room?.status === 'DRAWING';
- return isGuesser && isDrawing;
- }, [roundAssignedRole, room?.status]);
-
- const shouldHideQuizTitle = useMemo(() => {
- const isGuesser = roundAssignedRole === PlayerRole.GUESSER;
- const isDrawing = room?.status === 'DRAWING';
- return isGuesser && isDrawing;
- }, [roundAssignedRole, room?.status]);
-
- const shouldHideSizzlingTimer = useMemo(() => {
- const isPainters = roundAssignedRole === PlayerRole.DEVIL || roundAssignedRole === PlayerRole.PAINTER;
- const isDrawing = room?.status === 'DRAWING';
- const isGuessing = room?.status === 'GUESSING';
- return (isPainters && isDrawing) || isGuessing;
- }, [roundAssignedRole, room?.status]);
-
- const remainingTime = useMemo(() => {
- switch (room?.status) {
- case 'DRAWING':
- return timers.DRAWING ?? roomSettings?.drawTime;
- case 'GUESSING':
- return timers.GUESSING ?? 15;
- default:
- return 0;
- }
- }, [room?.status, timers, roomSettings?.drawTime]);
-
- const quizTitleText = useMemo(() => {
- if (room.status === RoomStatus.DRAWING) {
- return roundAssignedRole !== PlayerRole.GUESSER ? `${room.currentWord}` : '';
- }
- if (room.status === RoomStatus.GUESSING) {
- return roundAssignedRole !== PlayerRole.GUESSER ? `${room.currentWord} (맞히는중...)` : '맞혀보세요-!';
- }
- }, [room.status, room.currentWord, roundAssignedRole]);
return (
<>
{/* 구경꾼 전용 타이머 */}
-
-
- 화가들이 실력을 뽐내는중...
-
-
-

-
- {timers.DRAWING ?? roomSettings.drawTime - 5}
-
+ {checkShowBigTimer() && (
+
+
+ 화가들이 실력을 뽐내는중...
+
+
+

+
+ {getRemainingTime() ?? 0}
+
+
+ )}
+
+
+
+
+
-
-
-
-
>
);
};
diff --git a/client/src/components/room-setting/Setting.tsx b/client/src/components/room-setting/Setting.tsx
index 191d2426b..e354fe048 100644
--- a/client/src/components/room-setting/Setting.tsx
+++ b/client/src/components/room-setting/Setting.tsx
@@ -1,11 +1,11 @@
-import { HTMLAttributes, memo, useCallback, useEffect, useState } from 'react';
+import { HTMLAttributes, memo } from 'react';
import { RoomSettings } from '@troublepainter/core';
import { SettingContent } from '@/components/room-setting/SettingContent';
import { WordsThemeModalContent } from '@/components/room-setting/WordsThemeModalContent';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { SHORTCUT_KEYS } from '@/constants/shortcutKeys';
-import { gameSocketHandlers } from '@/handlers/socket/gameSocket.handler';
+import { useGameSetting } from '@/hooks/game/useGameSetting';
import { useModal } from '@/hooks/useModal';
import { useGameSocketStore } from '@/stores/socket/gameSocket.store';
import { cn } from '@/utils/cn';
@@ -27,44 +27,14 @@ export const ROOM_SETTINGS: RoomSettingItem[] = [
];
const Setting = memo(({ className, ...props }: HTMLAttributes
) => {
- // 개별 selector로 필요한 상태만 구독
+ const settingTool = useGameSetting();
+ const { checkCanSettingEdit, updateSetting, selectedValues } = settingTool;
+
const roomSettings = useGameSocketStore((state) => state.roomSettings);
- const isHost = useGameSocketStore((state) => state.isHost);
- const actions = useGameSocketStore((state) => state.actions);
+
// 모달
const { isModalOpened, openModal, closeModal, handleKeyDown } = useModal();
- const [selectedValues, setSelectedValues] = useState(
- roomSettings ?? {
- totalRounds: 5,
- maxPlayers: 5,
- drawTime: 30,
- },
- );
-
- useEffect(() => {
- if (!roomSettings) return;
- setSelectedValues(roomSettings);
- }, [roomSettings]);
-
- const handleSettingChange = useCallback(
- (key: keyof RoomSettings, value: string) => {
- const newSettings = {
- ...selectedValues,
- [key]: Number(value),
- };
- setSelectedValues(newSettings);
- void gameSocketHandlers.updateSettings({
- settings: { ...newSettings, drawTime: newSettings.drawTime + 5 },
- });
- actions.updateRoomSettings(newSettings);
- },
- [selectedValues, actions],
- );
-
- // 제시어 테마
- const headerText = roomSettings?.wordsTheme ? roomSettings.wordsTheme : 'Setting';
-
return (
) =
>
{/* Setting title */}
-
{headerText}
- {isHost && (
+
+ {roomSettings?.wordsTheme || 'Setting'}
+
+ {checkCanSettingEdit() && (