diff --git a/public/help-images/album_1.PNG b/public/help-images/album_1.PNG new file mode 100644 index 0000000..98db91a Binary files /dev/null and b/public/help-images/album_1.PNG differ diff --git a/public/help-images/album_2.PNG b/public/help-images/album_2.PNG new file mode 100644 index 0000000..13e8fb4 Binary files /dev/null and b/public/help-images/album_2.PNG differ diff --git a/public/help-images/canvas_1.PNG b/public/help-images/canvas_1.PNG new file mode 100644 index 0000000..59ba4c6 Binary files /dev/null and b/public/help-images/canvas_1.PNG differ diff --git a/public/help-images/canvas_2.PNG b/public/help-images/canvas_2.PNG new file mode 100644 index 0000000..765b244 Binary files /dev/null and b/public/help-images/canvas_2.PNG differ diff --git a/public/help-images/canvas_3.PNG b/public/help-images/canvas_3.PNG new file mode 100644 index 0000000..baf324d Binary files /dev/null and b/public/help-images/canvas_3.PNG differ diff --git a/public/help-images/chat_1.PNG b/public/help-images/chat_1.PNG new file mode 100644 index 0000000..9f23899 Binary files /dev/null and b/public/help-images/chat_1.PNG differ diff --git a/public/help-images/chat_2.PNG b/public/help-images/chat_2.PNG new file mode 100644 index 0000000..6edc7b0 Binary files /dev/null and b/public/help-images/chat_2.PNG differ diff --git a/public/help-images/chat_3.PNG b/public/help-images/chat_3.PNG new file mode 100644 index 0000000..6b3e56d Binary files /dev/null and b/public/help-images/chat_3.PNG differ diff --git a/public/help-images/final.PNG b/public/help-images/final.PNG new file mode 100644 index 0000000..1835f3a Binary files /dev/null and b/public/help-images/final.PNG differ diff --git a/public/help-images/group_1.PNG b/public/help-images/group_1.PNG new file mode 100644 index 0000000..e7a9352 Binary files /dev/null and b/public/help-images/group_1.PNG differ diff --git a/public/help-images/group_2.PNG b/public/help-images/group_2.PNG new file mode 100644 index 0000000..ed769eb Binary files /dev/null and b/public/help-images/group_2.PNG differ diff --git a/public/help-images/group_3.PNG b/public/help-images/group_3.PNG new file mode 100644 index 0000000..9dc435a Binary files /dev/null and b/public/help-images/group_3.PNG differ diff --git a/public/help-images/group_4.PNG b/public/help-images/group_4.PNG new file mode 100644 index 0000000..b1434a2 Binary files /dev/null and b/public/help-images/group_4.PNG differ diff --git a/public/help-images/group_5.PNG b/public/help-images/group_5.PNG new file mode 100644 index 0000000..8aa229e Binary files /dev/null and b/public/help-images/group_5.PNG differ diff --git a/public/help-images/group_6.PNG b/public/help-images/group_6.PNG new file mode 100644 index 0000000..e4e07a8 Binary files /dev/null and b/public/help-images/group_6.PNG differ diff --git a/public/help-images/group_7.PNG b/public/help-images/group_7.PNG new file mode 100644 index 0000000..a589a55 Binary files /dev/null and b/public/help-images/group_7.PNG differ diff --git a/public/help-images/imageguide_1.PNG b/public/help-images/imageguide_1.PNG new file mode 100644 index 0000000..ad420fc Binary files /dev/null and b/public/help-images/imageguide_1.PNG differ diff --git a/public/help-images/imageguide_10.PNG b/public/help-images/imageguide_10.PNG new file mode 100644 index 0000000..249e0b5 Binary files /dev/null and b/public/help-images/imageguide_10.PNG differ diff --git a/public/help-images/imageguide_2.PNG b/public/help-images/imageguide_2.PNG new file mode 100644 index 0000000..4c1ddd6 Binary files /dev/null and b/public/help-images/imageguide_2.PNG differ diff --git a/public/help-images/imageguide_3.PNG b/public/help-images/imageguide_3.PNG new file mode 100644 index 0000000..21e396d Binary files /dev/null and b/public/help-images/imageguide_3.PNG differ diff --git a/public/help-images/imageguide_4.PNG b/public/help-images/imageguide_4.PNG new file mode 100644 index 0000000..38b85d8 Binary files /dev/null and b/public/help-images/imageguide_4.PNG differ diff --git a/public/help-images/imageguide_5.PNG b/public/help-images/imageguide_5.PNG new file mode 100644 index 0000000..818c727 Binary files /dev/null and b/public/help-images/imageguide_5.PNG differ diff --git a/public/help-images/imageguide_6.PNG b/public/help-images/imageguide_6.PNG new file mode 100644 index 0000000..6130d39 Binary files /dev/null and b/public/help-images/imageguide_6.PNG differ diff --git a/public/help-images/imageguide_7.PNG b/public/help-images/imageguide_7.PNG new file mode 100644 index 0000000..534e817 Binary files /dev/null and b/public/help-images/imageguide_7.PNG differ diff --git a/public/help-images/imageguide_8.PNG b/public/help-images/imageguide_8.PNG new file mode 100644 index 0000000..4b4cc68 Binary files /dev/null and b/public/help-images/imageguide_8.PNG differ diff --git a/public/help-images/imageguide_9.PNG b/public/help-images/imageguide_9.PNG new file mode 100644 index 0000000..6dfbacd Binary files /dev/null and b/public/help-images/imageguide_9.PNG differ diff --git a/public/help-images/mypage_1.PNG b/public/help-images/mypage_1.PNG new file mode 100644 index 0000000..ed9a74f Binary files /dev/null and b/public/help-images/mypage_1.PNG differ diff --git a/public/help-images/mypage_2.PNG b/public/help-images/mypage_2.PNG new file mode 100644 index 0000000..a634be2 Binary files /dev/null and b/public/help-images/mypage_2.PNG differ diff --git a/public/help-images/pick_1.PNG b/public/help-images/pick_1.PNG new file mode 100644 index 0000000..3e9e89c Binary files /dev/null and b/public/help-images/pick_1.PNG differ diff --git a/public/help-images/pick_2.PNG b/public/help-images/pick_2.PNG new file mode 100644 index 0000000..9fd1c62 Binary files /dev/null and b/public/help-images/pick_2.PNG differ diff --git a/public/help-images/pick_3.PNG b/public/help-images/pick_3.PNG new file mode 100644 index 0000000..5f2ccf3 Binary files /dev/null and b/public/help-images/pick_3.PNG differ diff --git a/public/help-images/pick_4.PNG b/public/help-images/pick_4.PNG new file mode 100644 index 0000000..d6208d1 Binary files /dev/null and b/public/help-images/pick_4.PNG differ diff --git a/public/help-images/pick_5.PNG b/public/help-images/pick_5.PNG new file mode 100644 index 0000000..993d6e3 Binary files /dev/null and b/public/help-images/pick_5.PNG differ diff --git a/public/help-images/pick_6.PNG b/public/help-images/pick_6.PNG new file mode 100644 index 0000000..f834f97 Binary files /dev/null and b/public/help-images/pick_6.PNG differ diff --git a/public/help-images/tutorial.jpeg b/public/help-images/tutorial.jpeg new file mode 100644 index 0000000..801e106 Binary files /dev/null and b/public/help-images/tutorial.jpeg differ diff --git a/src/App.tsx b/src/App.tsx index 60e0508..2013ffc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,11 +24,14 @@ import HelpModalContent from './components/modal/HelpModalContent'; import CanvasEndedModal from './components/modal/CanvasEndedModal'; // CanvasEndedModal import 추가 import NotificationToast from './components/toast/NotificationToast'; // NotificationToast import 추가 import { useToastStore } from './store/toastStore'; // useToastStore import 추가 +import GameModalContent from './components/modal/GameModalContent'; type DecodedToken = { sub: { userId: string; nickName: string; + email?: string; + role?: string; }; jti: string; exp: number; @@ -52,6 +55,8 @@ function App() { closeMyPageModal, isGroupModalOpen, closeGroupModal, + isGameModalOpen, + closeGameModal, isCanvasModalOpen, closeCanvasModal, isAlbumModalOpen, @@ -105,6 +110,8 @@ function App() { const user = { userId: decodedToken.sub.userId, nickname: decodedToken.sub.nickName, + email: decodedToken.sub.email, + role: decodedToken.sub.role, }; console.log(user); setAuth(newAccessToken, user); @@ -158,9 +165,12 @@ function App() { - + + + + {isCanvasEndedModalOpen && } {/* NotificationToast 컴포넌트 추가 */} diff --git a/src/api/AdminAPI.ts b/src/api/AdminAPI.ts new file mode 100644 index 0000000..bc4c1c2 --- /dev/null +++ b/src/api/AdminAPI.ts @@ -0,0 +1,49 @@ +import { data } from 'react-router-dom'; +import apiClient from '../services/apiClient'; + +// 관리자 API 함수들 +export const AdminAPI = { + // 캔버스 리스트 + getCanvases: async () => { + const response = await apiClient.get('/admin/canvas/list'); + // 백엔드 응답의 canvasId를 id로 매핑 + return response.data.map((canvas: any) => ({ + ...canvas, + id: canvas.canvasId, + })); + }, + + // 캔버스 생성 + createCanvas: async (canvasData: { + title: string; + type: string; + // created_at: string; + started_at: string; + ended_at: string | null; + size_x: number; + size_y: number; + }) => { + const response = await apiClient.post('/admin/canvas', canvasData); + return response.data; + }, + + // 캔버스 삭제 + deleteCanvas: async (canvasId: number) => { + const response = await apiClient.delete('/admin/canvas', { + data: { + canvasId: canvasId, + }, + }); + + return response.data; + }, + + // 게임 강제종료 + forceEnd: async (canvasId: number) => { + // POST 요청의 body는 두 번째 인자로 바로 객체를 전달합니다. + const response = await apiClient.post('/admin/force_end', { + canvasId: canvasId, + }); + return response.data; + }, +}; diff --git a/src/api/CanvasAPI.ts b/src/api/CanvasAPI.ts index ce7061d..31c1ebe 100644 --- a/src/api/CanvasAPI.ts +++ b/src/api/CanvasAPI.ts @@ -93,6 +93,18 @@ export const canvasService = { } }, + // 시작전 게임 캔버스 목록 조회 (이동 시 사용) + async getGameCanvases(): Promise { + try { + const response = await apiClient.get(`/game/list`); + console.log(response.data); + return response.data; + } catch (error) { + console.error('Error fetching canvas:', error); + throw new Error('게임 캔버스 정보를 불러오는데 실패했습니다.'); + } + }, + // 특정 캔버스 조회 (이동 시 사용) async getCanvas(canvasId: number): Promise { try { diff --git a/src/auth/AuthCallbackPage.tsx b/src/auth/AuthCallbackPage.tsx index 3b86ad2..4a28d89 100644 --- a/src/auth/AuthCallbackPage.tsx +++ b/src/auth/AuthCallbackPage.tsx @@ -45,6 +45,9 @@ export default function AuthCallbackPage() { console.log(authResult); if (authResult?.accessToken && authResult?.user) { + // 사용자 정보에 role 설정 + setAuth(authResult.accessToken, authResult.user); + // ✨ 1. 객체를 JSON 문자열로 변환합니다. const authResultString = JSON.stringify(authResult); diff --git a/src/components/SocketIntegration.tsx b/src/components/SocketIntegration.tsx index e2ef513..75aaf3a 100644 --- a/src/components/SocketIntegration.tsx +++ b/src/components/SocketIntegration.tsx @@ -6,6 +6,9 @@ interface SocketIntegrationProps { sourceCanvasRef: React.RefObject; draw: () => void; canvas_id: string; + onPixelReceived?: (data: { + pixels: Array<{ x: number; y: number; color: string }>; + }) => void; onCooldownReceived?: (cooldown: { cooldown: boolean; remaining: number; @@ -28,23 +31,38 @@ export const usePixelSocket = ({ sourceCanvasRef, draw, canvas_id, + onPixelReceived, onCooldownReceived, }: SocketIntegrationProps) => { const handlePixelReceived = useCallback( - (pixel: { x: number; y: number; color: string }) => { + (data: any) => { + // 외부에서 제공된 onPixelReceived 콜백이 있으면 사용 + if (onPixelReceived) { + onPixelReceived(data); + return; + } + + // 기본 동작: 소스 캔버스에 직접 그리기 + const { pixels } = data; const sourceCtx = sourceCanvasRef.current?.getContext('2d'); if (sourceCtx) { - sourceCtx.fillStyle = pixel.color; - sourceCtx.fillRect(pixel.x, pixel.y, 1, 1); + // 각 픽셀에 대해 그리기 + pixels.forEach((pixel: { x: number; y: number; color: string }) => { + // 소스 캔버스에 픽셀 그리기 + sourceCtx.fillStyle = pixel.color; + sourceCtx.fillRect(pixel.x, pixel.y, 1, 1); + }); + + // 캔버스 다시 그리기 draw(); } }, - [sourceCanvasRef, draw] + [sourceCanvasRef, draw, onPixelReceived] ); const { sendPixel } = useSocket( - handlePixelReceived, canvas_id, + handlePixelReceived, onCooldownReceived ); diff --git a/src/components/canvas/CanvasUIMobile.tsx b/src/components/canvas/CanvasUIMobile.tsx index 20c34d8..a3531c6 100644 --- a/src/components/canvas/CanvasUIMobile.tsx +++ b/src/components/canvas/CanvasUIMobile.tsx @@ -66,6 +66,7 @@ export default function CanvasUIMobile({ openMyPageModal, openGroupModal, openHelpModal, + openGameModal, } = useModalStore(); // 드롭다움 열림, 닫힘 상태 @@ -335,6 +336,31 @@ export default function CanvasUIMobile({ {isLoggedIn ? '마이페이지' : '로그인'} + {/* 그룹 버튼 */} +
+ + + 그룹 + +
{/* 캔버스 버튼 */}
- {/* 앨범 버튼 */} + {/* 게임 버튼 */}
- 앨범 + 게임
- - {/* 그룹 버튼 */} + {/* 앨범 버튼 */}
- 그룹 + 앨범
- {/* BGM 버튼 */}
+ {/* 도움말 버튼 - 메뉴 버튼 오른쪽에 배치 */} + + {/* isMenuOpen이 true일 때만 드롭다운 메뉴가 보입니다. */} {isMenuOpen && (
@@ -219,39 +227,38 @@ export default function CanvasUIPC({ {/* 캔버스 버튼 */} - {/* 갤러리 버튼 */} + {/* 게임 버튼 */} - {/* BGM 버튼 */} + {/* 갤러리 버튼 */} - {/* 도움말 버튼 */} + {/* BGM 버튼 */}
diff --git a/src/components/canvas/UserCount.tsx b/src/components/canvas/UserCount.tsx index a773b65..c718900 100644 --- a/src/components/canvas/UserCount.tsx +++ b/src/components/canvas/UserCount.tsx @@ -36,9 +36,9 @@ export default function UserCount() { }, [canvas_id, handleUserCountChange]); // handleUserCountChange가 변경될 때만 재실행 return ( -
+
-
+
+ {/* 툴팁 - 가로로 표시 */} +
+ 전체 접속자 수 +
-
+
+ {/* 툴팁 - 가로로 표시 */} +
+ 현재 캔버스 접속자 수 +
diff --git a/src/components/game/GameCanvas.tsx b/src/components/game/GameCanvas.tsx index 8d01c06..15ab4b5 100644 --- a/src/components/game/GameCanvas.tsx +++ b/src/components/game/GameCanvas.tsx @@ -74,6 +74,7 @@ function GameCanvas({ try_count: number; dead: boolean; }> | null>(null); // 게임 결과 + const [userColor, setUserColor] = useState('#FF5733'); // 사용자 색상 (서버에서 받아올 예정) const [playExplosion] = useSound('/explosion.mp3', { volume: 0.2, @@ -93,6 +94,17 @@ function GameCanvas({ volume: 0.25, loop: true, }); + const [isGameMusicMuted, setIsGameMusicMuted] = useState(false); + + const toggleGameMusic = useCallback(() => { + if (!isGameMusicMuted) { + stopGameMusic(); + setIsGameMusicMuted(true); + } else { + playGameMusic(); + setIsGameMusicMuted(false); + } + }, [isGameMusicMuted, stopGameMusic, playGameMusic]); const [playClick] = useSound('/click.mp3', { volume: 0.7 }); const rootRef = useRef(null); @@ -1151,24 +1163,51 @@ function GameCanvas({ {isGameStarted && ( <> {/* 나가기 버튼 및 생명 표시 */} -
- - -
+ {/* PC 버전: 나가기 버튼 및 생명 표시 */} + {!isMobile && ( +
+ + +
+ )} + + {/* 모바일 버전: 나가기 버튼, 생명 표시 */} + {isMobile && ( +
+ + +
+ )} + {/* PC 버전: BGM 버튼 (좌측 하단) */} + {!isMobile && ( +
+ +
+ )} +
+
+

게임 모드 이동

+

+ 이동할 캔버스를 선택해주세요. +

+
+ +
+
+
+

게임 캔버스

+ {canvases?.length > 2 && ( +
+ + +
+ )} +
+ + {canvases?.length === 0 ? ( +
+ + + +

예정된 게임 캔버스가 없습니다.

+
+ ) : ( +
handleMouseDown(e, 'event')} + onMouseMove={(e) => handleMouseMove(e, 'event')} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} + > + {canvases + .map((canvas) => { + const timeInfo = canvas.ended_at + ? getTimeRemaining(canvas.ended_at, canvas.started_at) + : null; + return { canvas, timeInfo }; + }) + .filter(({ timeInfo }) => timeInfo?.isUpcoming) + .sort((a, b) => { + if (a.timeInfo?.targetDate && b.timeInfo?.targetDate) { + return ( + a.timeInfo.targetDate.getTime() - + b.timeInfo.targetDate.getTime() + ); + } + return 0; + }) + .map(({ canvas, timeInfo }) => { + return ( +
handleCanvasSelect(e, canvas.canvasId)} + className='group canvas-rainbow-border block min-w-[200px] cursor-pointer transition-all duration-300 hover:shadow-xl hover:shadow-gray-900/20' + > +
+

+ {canvas.title}{' '} + {canvas.canvasId === Number(canvas_id) && '📍'} +

+

+ {timeInfo + ? timeInfo.text + : formatDate(canvas.created_at)} +

+
+ + {canvas.size_x} × {canvas.size_y} + + {canvas.status && ( + + {canvas.status} + + )} + + {canvas.type} + +
+
+
+ ); + })} +
+ )} +
+
+
+ + ); +}; + +export default GameModalContent; diff --git a/src/components/modal/HelpModalContent.tsx b/src/components/modal/HelpModalContent.tsx index ff574c4..24b00e4 100644 --- a/src/components/modal/HelpModalContent.tsx +++ b/src/components/modal/HelpModalContent.tsx @@ -1,221 +1,443 @@ -import React from 'react'; -import Slider from 'react-slick'; +import React, { useState, useEffect, useRef } from 'react'; import { useViewport } from '../../hooks/useViewport'; +import Slider from 'react-slick'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +// 캐러셀 커스텀 스타일 +const carouselStyles = ` + .help-carousel .slick-prev, + .help-carousel .slick-next { + z-index: 10; + width: 40px; + height: 40px; + } + + .help-carousel .slick-prev { + left: 10px; + } + + .help-carousel .slick-next { + right: 10px; + } + + .help-carousel .slick-prev:before, + .help-carousel .slick-next:before { + font-size: 30px; + opacity: 0.8; + } + + .help-carousel .slick-dots { + bottom: -30px; + } + + .carousel-container { + padding: 0; + overflow: hidden; + width: 100%; + } + + .help-carousel .slick-slide { + padding: 0; + } +`; + +type HelpModalContentProps = { + onClose?: () => void; +}; + +type TabType = 'pixel' | 'chat' | 'imageguide' | 'group' | 'canvas' | 'mypage'; -export default function HelpModalContent() { +// 캐러셀 아이템 타입 정의 +type CarouselItem = { + title: string; + description: string; + image: string; // 이미지 경로 또는 placeholder + color: string; +}; + +export default function HelpModalContent({ onClose }: HelpModalContentProps) { + const [activeTab, setActiveTab] = useState('pixel'); const { width } = useViewport(); const isMobile = width < 768; + const sliderRef = useRef(null); - const settings = { + // 캐러셀 설정 + const sliderSettings = { dots: true, infinite: true, speed: 500, slidesToShow: 1, slidesToScroll: 1, - autoplay: true, - autoplaySpeed: 5000, + autoplay: false, + arrows: true, + adaptiveHeight: true, + className: 'help-carousel', }; - const sections = [ - { - title: '게임 소개', - iconColor: 'blue', - content: ( -

- 여러 사용자가 함께 참여하여 하나의 캔버스에 픽셀 아트를 그리는 협업 - 게임입니다. 각자의 창의성을 발휘하여 멋진 작품을 만들어보세요! -

(모바일도 게임을 즐길 수 있지만 PC에 최적화 되었어요!)

-

- ), - }, - { - title: '기본 조작법', - iconColor: 'green', - content: isMobile ? ( -
    -
  • - - - 터치: 픽셀 선택 - -
  • -
  • - - - 두 손가락으로 확대/축소: 캔버스 확대/축소 - -
  • -
  • - - - 한 손가락으로 드래그: 캔버스 이동 - -
  • -
  • - - - 우측 최상단 컬러 피커: 원하는 색상 선택 - -
  • -
  • - - - 우측 최상단 체크 버튼: 색상 칠하기 - -
  • -
- ) : ( -
    -
  • - - - 클릭: 선택한 색상으로 픽셀 칠하기 - -
  • -
  • - - - 마우스 휠: 캔버스 확대/축소 - -
  • -
  • - - - 드래그: 캔버스 이동 - -
  • -
  • - - - 컬러 팔레트: 원하는 색상 선택 - -
  • -
  • - - - 키보드 방향키: 선택 픽셀 이동 - -
  • -
  • - - - Enter: 선택 색상으로 픽셀 칠하기 - -
  • -
- ), - }, - { - title: '게임 규칙', - iconColor: 'yellow', - content: ( -
    -
  • - - - 쿨타임: 픽셀을 칠한 후 일정 시간 대기 필요 - -
  • -
  • - - - 로그인 필수: 픽셀을 칠하려면 로그인이 필요합니다 - -
  • -
  • - - - 실시간 동기화: 다른 사용자의 작업이 실시간으로 - 반영됩니다 - -
  • -
  • - - - 협업 정신: 다른 사용자의 작품을 존중해주세요 - -
  • -
- ), - }, - { - title: '추가 기능', - iconColor: 'purple', - content: ( -
    -
  • - - - 이미지 업로드: 참고용 이미지를 업로드할 수 - 있습니다 - -
  • -
  • - - - 투명도 조절: 업로드한 이미지의 투명도를 조절할 수 - 있습니다 - -
  • -
  • - - - 채팅: 다른 사용자들과 실시간으로 소통할 수 - 있습니다 - -
  • -
  • - - - 그룹 기능: 그룹을 만들어 함께 작업할 수 있습니다 - -
  • -
- ), - }, - { - title: '유용한 팁', - iconColor: 'red', - content: ( -
    -
  • - - 작은 디테일부터 시작해서 점진적으로 확장해보세요 -
  • -
  • - - 다른 사용자들과 협력하여 더 큰 작품을 만들어보세요 -
  • -
  • - - 채팅을 통해 작업 계획을 공유해보세요 -
  • -
- ), - }, - ]; + const addSlideNumbers = ( + items: CarouselItem[], + color: string + ): CarouselItem[] => { + return items.map((item, index) => ({ + ...item, + title: `${index + 1}. ${item.title}`, + color, + })); + }; + + const pixelCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '휠로 확대/축소', + description: + '마우스 휠을 사용해 원하는 범위를 확대하거나 축소할 수 있어요.', + image: '/help-images/pick_1.PNG', + color: '', + }, + { + title: '화면 이동하기', + description: + '마우스를 드래그하면 원하는 위치로 자유롭게 이동할 수 있어요.', + image: '/help-images/pick_2.PNG', + color: '', + }, + { + title: '픽셀 색상 고르기', + description: '픽셀을 클릭한 뒤, 팔레트에서 원하는 색을 선택해보세요.', + image: '/help-images/pick_3.PNG', + color: '', + }, + { + title: '색칠 확정하기', + description: '체크 버튼을 눌러 선택한 색으로 픽셀을 확정할 수 있어요.', + image: '/help-images/pick_4.PNG', + color: '', + }, + { + title: '쿨타임 안내', + description: '색칠 후에는 일정 시간(3초) 쿨타임이 적용됩니다.', + image: '/help-images/pick_5.PNG', + color: '', + }, + { + title: '쿨타임 후 다시 색칠', + description: '쿨타임이 끝나면 다음 픽셀을 자유롭게 칠할 수 있어요.', + image: '/help-images/pick_6.PNG', + color: '', + }, + ], + 'yellow' + ); + + const chatCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '채팅창 열기', + description: + '화면 좌측 하단의 채팅 버튼을 눌러 채팅을 시작할 수 있어요.', + image: '/help-images/chat_1.PNG', + color: '', + }, + { + title: '전체 그룹 채팅', + description: '처음 입장 시, 전체 그룹 채팅방으로 자동 연결됩니다.', + image: '/help-images/chat_2.PNG', + color: '', + }, + { + title: '그룹 이동하기', + description: + '그룹을 직접 생성하거나 참가할 수 있으며, 채팅창에서 확인할 수 있어요.', + image: '/help-images/chat_3.PNG', + color: '', + }, + ], + 'green' + ); + + const imageGuideCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '이미지 가이드 시작하기', + description: + '채팅 버튼 오른쪽의 아이콘을 눌러 이미지 가이드를 시작하세요.', + image: '/help-images/imageguide_1.PNG', + color: '', + }, + { + title: '이미지 업로드', + description: + '참고하고 싶은 이미지를 업로드해 캔버스 위에 덧붙일 수 있어요.', + image: '/help-images/imageguide_2.PNG', + color: '', + }, + { + title: '이미지 위치 조정', + description: + '마우스로 크기와 위치를 조절하며 캔버스에 잘 맞게 배치하세요.', + image: '/help-images/imageguide_3.PNG', + color: '', + }, + { + title: '캔버스 시점 조절', + description: '마우스로 캔버스를 이동하거나 확대/축소할 수 있어요.', + image: '/help-images/imageguide_4.PNG', + color: '', + }, + { + title: '투명도 조절하기', + description: + '이미지가 너무 진하거나 흐릴 땐, 투명도 슬라이더를 조절해보세요.', + image: '/help-images/imageguide_5.PNG', + color: '', + }, + { + title: '이미지 고정하기', + description: + '위치와 크기를 조절한 후, [확정] 버튼으로 이미지를 고정하세요.', + image: '/help-images/imageguide_6.PNG', + color: '', + }, + { + title: '그림 그리기', + description: + '이제 이미지를 참고하여 픽셀을 하나씩 채워 그림을 완성해보세요.', + image: '/help-images/imageguide_7.PNG', + color: '', + }, + { + title: '이미지 가이드 따라 그리기', + description: + '투명도를 조절하며 원본 이미지를 참고해 정확하게 색칠해보세요.', + image: '/help-images/imageguide_8.PNG', + color: '', + }, + { + title: '컬러피커 활용하기', + description: + '컬러피커 도구로 이미지에서 직접 색상을 추출하여 사용할 수 있어요.', + image: '/help-images/imageguide_9.PNG', + color: '', + }, + { + title: '이미지 저장 안내', + description: + '개인 업로드 이미지는 저장되지 않아요. 이미지 저장 및 공유를 원하시면 그룹 기능을 이용해주세요! ', + image: '/help-images/imageguide_10.PNG', + color: '', + }, + ], + 'blue' + ); + + const groupCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '그룹 만들기', + description: + '원하는 이름과 최대 인원수를 설정해 나만의 그룹을 만들 수 있어요.', + image: '/help-images/group_1.PNG', + color: '', + }, + { + title: '그룹 참여하기', + description: '다른 사용자가 만든 그룹을 선택해 함께 참여할 수 있어요.', + image: '/help-images/group_2.PNG', + color: '', + }, + { + title: '그룹 이미지 공유 기능', + description: + '그룹장은 모든 멤버가 함께 볼 수 있는 이미지를 업로드할 수 있어요.', + image: '/help-images/group_3.PNG', + color: '', + }, + { + title: '그룹 이미지 업로드', + description: + '개인 이미지와 동일한 방식으로 그룹에서 공유할 이미지를 업로드합니다.', + image: '/help-images/group_4.PNG', + color: '', + }, + { + title: '그룹 이미지 동기화', + description: + '팀원들은 동기화 버튼을 눌러 그룹장이 업로드한 이미지를 불러올 수 있어요.', + image: '/help-images/group_5.PNG', + color: '', + }, + { + title: '이미지 동기화 완료', + description: + '동기화된 이미지는 모든 그룹원의 캔버스에 동일하게 표시되어 협업이 가능해요.', + image: '/help-images/group_6.PNG', + color: '', + }, + { + title: '좌표 공유하기', + description: + '채팅창에서 좌표를 공유하면 클릭 시 해당 위치로 바로 이동할 수 있어요.', + image: '/help-images/group_7.PNG', + color: '', + }, + ], + 'purple' + ); + + const canvasCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '캔버스 선택하기', + description: + '상단 메뉴에서 캔버스 아이콘을 클릭해 다양한 캔버스를 확인해보세요.', + image: '/help-images/canvas_1.PNG', + color: '', + }, + { + title: '캔버스 모드 선택', + description: + '일반, 이벤트, 게임 등 다양한 모드의 캔버스를 즐길 수 있어요.', + image: '/help-images/canvas_2.PNG', + color: '', + }, + { + title: '특별 캔버스 참여', + description: + '이벤트 모드에서는 흑백, 제한 색상 등 특별한 규칙의 캔버스가 제공됩니다.', + image: '/help-images/canvas_3.PNG', + color: '', + }, + ], + 'teal' + ); + + const mypageCarouselItems: CarouselItem[] = addSlideNumbers( + [ + { + title: '내 프로필 보기', + description: + '상단 메뉴의 프로필 아이콘을 클릭해 내 정보를 확인할 수 있어요.', + image: '/help-images/mypage_1.PNG', + color: '', + }, + { + title: '참여한 작품들', + description: + '내가 참여했던 모든 캔버스 기록과 통계를 한눈에 볼 수 있어요.', + image: '/help-images/mypage_2.PNG', + color: '', + }, + ], + 'orange' + ); - const renderSections = () => { - return sections.map((section, index) => ( -
-

- {section.title} -

- {section.content} -
- )); + // 탭 변경 시 슬라이더 초기화 + useEffect(() => { + if (sliderRef.current) { + setTimeout(() => { + if (sliderRef.current) { + sliderRef.current.slickGoTo(0); + } + }, 0); + } + }, [activeTab]); + + // 현재 탭에 따른 캐러셀 아이템 선택 + const getCurrentCarouselItems = () => { + switch (activeTab) { + case 'pixel': + return pixelCarouselItems; + case 'chat': + return chatCarouselItems; + case 'imageguide': + return imageGuideCarouselItems; + case 'group': + return groupCarouselItems; + case 'canvas': + return canvasCarouselItems; + case 'mypage': + return mypageCarouselItems; + default: + return pixelCarouselItems; + } + }; + + // 탭 변경 핸들러 + const handleTabChange = (tab: TabType) => { + setActiveTab(tab); + }; + + // 캐러셀 아이템 렌더링 함수 + const renderCarouselItem = (item: CarouselItem) => { + return ( +
+
+ {/* 이미지 */} +
+ {item.title} { + // 이미지 로드 실패 시 플레이스홀더 표시 + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + const svg = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg' + ); + svg.setAttribute('class', 'h-16 w-16'); + svg.setAttribute('fill', 'none'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('stroke', 'currentColor'); + + const path = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ); + path.setAttribute('stroke-linecap', 'round'); + path.setAttribute('stroke-linejoin', 'round'); + path.setAttribute('stroke-width', '1'); + path.setAttribute( + 'd', + 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z' + ); + + svg.appendChild(path); + parent.appendChild(svg); + } + }} + /> +
+ + {/* 제목과 설명 */} +

+ {item.title} +

+

+ {item.description} +

+
+
+ ); }; return ( -
- {/* 헤더 */} -
-
-
+
+ {/* 인라인 스타일 추가 */} + + + {/* 헤더 - 고정 높이 */} +
+
+
-

게임 가이드

-

픽셀 아트 게임의 규칙과 사용법

+

+ 게임 가이드 +

+

+ 픽셀 아트 게임의 규칙과 사용법 +

+
- {/* 콘텐츠 */} - {isMobile ? ( - - {sections.map((section, index) => ( -
-

- {section.title} -

- {section.content} -
- ))} -
- ) : ( -
- {renderSections()} + {/* 탭 네비게이션 - 고정 높이 */} +
+
+ + + + + +
- )} +
- {/* 푸터 */} -
-

- 즐거운 픽셀 아트 여행을 시작해보세요! 🎨 -

+ {/* 콘텐츠 영역 - 스크롤 가능 */} +
+
+ + {getCurrentCarouselItems().map((item, index) => ( +
{renderCarouselItem(item)}
+ ))} +
+
+ + {/* 푸터 영역 제거 */}
); -} +} \ No newline at end of file diff --git a/src/components/modal/LoginModalContent.tsx b/src/components/modal/LoginModalContent.tsx index 9854f7f..3c9dbf3 100644 --- a/src/components/modal/LoginModalContent.tsx +++ b/src/components/modal/LoginModalContent.tsx @@ -8,20 +8,19 @@ type LoginModalContentProps = { }; export default function LoginModalContent({ onClose }: LoginModalContentProps) { - // 로그인 폼 자체의 상태를 스스로 관리합니다. - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [randomName, setRandomname] = useState(generateRandomNickname()); + const [nickname, setNickname] = useState(generateRandomNickname()); + const isLoginDisabled = nickname.trim().length === 0; const handleLogin = () => { - guestLogin(randomName); + if (isLoginDisabled) return; + guestLogin(nickname.trim()); onClose?.(); }; const handleOAuthLogin = (provider: 'google' | 'naver' | 'kakao') => { sessionStorage.setItem( 'redirectPath', - window.location.pathname + window.location.search + window.location.pathname + window.location.search, ); authService.redirectToProvider(provider); }; @@ -37,10 +36,11 @@ export default function LoginModalContent({ onClose }: LoginModalContentProps) {
setUsername(e.target.value)} + placeholder='닉네임을 입력하세요 (8자 이하)' + className='bg-gray-900 p-2 text-center text-white placeholder-gray-500 focus:outline-none' + value={nickname} + onChange={(e) => setNickname(e.target.value.replace(/\s/g, ''))} + maxLength={8} />
@@ -49,7 +49,10 @@ export default function LoginModalContent({ onClose }: LoginModalContentProps) {
diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx index cf4d411..5aabd0e 100644 --- a/src/components/modal/Modal.tsx +++ b/src/components/modal/Modal.tsx @@ -5,9 +5,10 @@ type ModalProps = { isOpen: boolean; onClose: () => void; children: React.ReactNode; + fullWidth?: boolean; }; -export default function Modal({ isOpen, onClose, children }: ModalProps) { +export default function Modal({ isOpen, onClose, children, fullWidth = false }: ModalProps) { if (!isOpen) return null; return ReactDOM.createPortal( @@ -16,7 +17,7 @@ export default function Modal({ isOpen, onClose, children }: ModalProps) { onClick={onClose} >
e.stopPropagation()} style={{ transitionProperty: 'height, min-height, max-height, transform', diff --git a/src/components/modal/MyPageModalContent.tsx b/src/components/modal/MyPageModalContent.tsx index f9f1a21..12ed69f 100644 --- a/src/components/modal/MyPageModalContent.tsx +++ b/src/components/modal/MyPageModalContent.tsx @@ -4,14 +4,16 @@ import React, { useEffect, useState } from 'react'; import { useAuthStore } from '../../store/authStrore'; import { authService } from '../../services/authService'; import { useModalStore } from '../../store/modalStore'; +import { useNavigate } from 'react-router-dom'; import { myPageService, type UserInfoResponse, } from '../../services/myPageService'; export default function MyPageModalContent() { - const { isLoggedIn, clearAuth } = useAuthStore(); + const { isLoggedIn, clearAuth, user } = useAuthStore(); const { closeMyPageModal, openLoginModal } = useModalStore(); + const navigate = useNavigate(); const [userInfo, setUserInfo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -117,14 +119,14 @@ export default function MyPageModalContent() { )} {canvas.canvasId !== 1 ? (

- 시도 : {canvas.try_count} / 점유 : { - canvas.own_count !== null ? - canvas.own_count : - + 시도 : {canvas.try_count} / 점유 :{' '} + {canvas.own_count !== null ? ( + canvas.own_count + ) : ( + 집계중 - - } + )}

) : (

@@ -144,7 +146,18 @@ export default function MyPageModalContent() {

{/* Footer */} -
+
+ {(user?.role === 'admin' || user?.role === 'ADMIN') && ( + + )} +
+ +
+
+

총 사용자

+

{stats.totalUsers}

+
+
+

활성 캔버스

+

{stats.activeCanvases}

+
+
+ +
+
+

관리 메뉴

+
+ + +
+
+ +
+

최근 활동

+
+

아직 활동 내역이 없습니다.

+
+
+
+
+
+ ); +}; + +export default AdminDashboard; diff --git a/src/pages/admin/CanvasManagement.tsx b/src/pages/admin/CanvasManagement.tsx new file mode 100644 index 0000000..b1e4b95 --- /dev/null +++ b/src/pages/admin/CanvasManagement.tsx @@ -0,0 +1,367 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { AdminAPI } from '../../api/AdminAPI'; +import { useAuthStore } from '../../store/authStrore'; + +// Canvas 인터페이스에 id 추가 +interface Canvas { + id: number; + title: string; + type: string; + // created_at: string; + started_at: string; + ended_at: string | null; + size_x: number; + size_y: number; +} + +const CanvasManagement: React.FC = () => { + const [canvases, setCanvases] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [newCanvas, setNewCanvas] = useState({ + title: '새 캔버스', + type: 'public', + size_x: 100, + size_y: 100, + start_delay: 0, // 시작 지연 시간 (초) + game_duration: 60, // 게임 진행 시간 (초) + }); + const navigate = useNavigate(); + const { user, isLoggedIn } = useAuthStore(); + + useEffect(() => { + // Mock API 테스트 시 아래 인증 로직을 주석 처리하면 편합니다. + if (!isLoggedIn || (user?.role !== 'admin' && user?.role !== 'ADMIN')) { + toast.error('관리자 권한이 필요합니다.'); + navigate('/'); + return; + } + fetchCanvases(); + }, [navigate, isLoggedIn, user]); + + const fetchCanvases = async () => { + setIsLoading(true); + try { + const data = await AdminAPI.getCanvases(); + const now = new Date(); + const activeCanvases = (data || []).filter((canvas: Canvas) => { + if (!canvas.ended_at) { + return true; // ended_at이 null이면 활성 캔버스 + } + const endedAt = new Date(canvas.ended_at); + return endedAt > now; // ended_at이 현재 시간보다 미래이면 활성 캔버스 + }); + setCanvases(activeCanvases); + } catch (error) { + console.error('캔버스 데이터 로드 오류:', error); + toast.error('캔버스 데이터를 불러오는데 실패했습니다.'); + setCanvases([]); + } finally { + setIsLoading(false); + } + }; + + const handleCreateCanvas = async (e: React.FormEvent) => { + e.preventDefault(); + + const now = new Date(); + const startedAt = new Date(now.getTime() + newCanvas.start_delay * 1000); + const endedAt = new Date( + startedAt.getTime() + newCanvas.game_duration * 1000 + ); + + const canvasData = { + // Mock API는 id가 필요하므로 임시로 생성합니다. + // 실제 백엔드에서 id를 자동 생성한다면 이 부분은 제외해야 합니다. + title: newCanvas.title, + type: newCanvas.type, + size_x: newCanvas.size_x, + size_y: newCanvas.size_y, + started_at: startedAt.toISOString().slice(0, -5) + 'Z', + ended_at: endedAt.toISOString().slice(0, -5) + 'Z', + }; + + try { + console.log('생성 요청 canvasData:', canvasData); + const createdCanvas = await AdminAPI.createCanvas(canvasData); + toast.success('캔버스가 성공적으로 생성되었습니다.'); + setNewCanvas({ + title: '새 캔버스', + type: 'public', + size_x: 100, + size_y: 100, + start_delay: 0, + game_duration: 60, + }); + // fetchCanvases() 대신, 생성된 캔버스를 직접 목록에 추가합니다. + setCanvases((prevCanvases) => [...prevCanvases, createdCanvas]); + } catch (error) { + console.error('캔버스 생성 오류:', error); + toast.error('캔버스 생성에 실패했습니다.'); + } + }; + + const handleDeleteCanvas = async (canvasId: number) => { + if (!window.confirm(`ID:${canvasId} 캔버스를 정말로 삭제하시겠습니까?`)) { + return; + } + try { + await AdminAPI.deleteCanvas(canvasId); + toast.success('캔버스가 성공적으로 삭제되었습니다.'); + fetchCanvases(); + } catch (error) { + console.error('캔버스 삭제 오류:', error); + toast.error('캔버스 삭제에 실패했습니다.'); + } + }; + + // 강제 종료 핸들러 추가 + const handleForceEnd = async (canvasId: number) => { + if (!window.confirm(`ID:${canvasId} 게임을 강제 종료하시겠습니까?`)) { + return; + } + try { + await AdminAPI.forceEnd(canvasId); + toast.info('게임이 강제 종료되었습니다.'); + fetchCanvases(); + } catch (error) { + console.error('강제 종료 오류:', error); + toast.error('강제 종료에 실패했습니다.'); + } + }; + + // UTC 날짜를 한국 시간(KST)으로 변환하는 함수 + // 추후 연결시 KST 함수 제외 + const formatDate = (dateString: string | null) => { + if (!dateString) return '-'; + return new Date(dateString).toLocaleString('ko-KR', { + timeZone: 'Asia/Seoul', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + }; + + // 캔버스 상태 계산 함수 + const getCanvasStatus = (canvas: Canvas) => { + const now = new Date(); + const startTime = new Date(canvas.started_at); + + if (canvas.ended_at && new Date(canvas.ended_at) < now) { + return 종료; + } + if (startTime > now) { + return 예정; + } + return 진행중; + }; + + const handleInputChange = ( + e: React.ChangeEvent + ) => { + const { name, value, type } = e.target; + const isNumberInput = type === 'number'; + + setNewCanvas((prev) => ({ + ...prev, + [name]: isNumberInput ? Number(value) : value, + })); + }; + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + return ( +
+
+
+

캔버스 관리

+ +
+ + {/* 새 캔버스 생성 폼 */} +
+

새 캔버스 생성

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + {/* 캔버스 목록 */} +
+
+

캔버스 목록

+ +
+ {canvases.length === 0 ? ( +

등록된 캔버스가 없습니다.

+ ) : ( +
+ + + + + + + + + + + + + + + {canvases.map((canvas) => ( + + + + + + + + + + + ))} + +
ID제목타입상태크기시작일 (KST)종료일 (KST)작업
{canvas.id}{canvas.title}{canvas.type}{getCanvasStatus(canvas)} + {canvas.size_x} x {canvas.size_y} + + {formatDate(canvas.started_at)} + + {formatDate(canvas.ended_at)} + + + +
+
+ )} +
+
+
+ ); +}; + +export default CanvasManagement; diff --git a/src/router/router.tsx b/src/router/router.tsx index e3f4295..d8955e1 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -1,9 +1,11 @@ -// src/router/Router.tsx (새 파일) +// src/router/Router.tsx import React from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import App from '../App'; import AuthCallbackPage from '../auth/AuthCallbackPage'; +import AdminDashboard from '../pages/admin/AdminDashboard'; +import CanvasManagement from '../pages/admin/CanvasManagement'; export default function Router() { return ( @@ -13,7 +15,10 @@ export default function Router() { } /> {/* ✨ OAuth 콜백을 처리할 전용 경로 */} } /> - {/* 나중에 추가될 다른 페이지 경로들... */} + + {/* 관리자 페이지 경로 */} + } /> + } /> ); diff --git a/src/services/authService.ts b/src/services/authService.ts index 49e923d..cd8add7 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -20,6 +20,8 @@ type AuthResult = { user: { userId: string; nickname?: string; + email?: string; + role?: string; }; }; @@ -27,6 +29,7 @@ type DecodedToken = { sub: { userId: string; nickName: string; + role?: string; }; jti: string; exp: number; @@ -68,6 +71,7 @@ export const authService = { const user = { userId: decodedToken.sub.userId, nickname: decodedToken.sub.nickName, + role: decodedToken.sub.role, }; console.log('응답결과:', user); // 응답에서 AT와 사용자 정보를 추출하여 반환 @@ -89,9 +93,17 @@ export const authService = { const response = await apiClient.post('/auth/refresh'); const authHeader = response.headers['authorization']; const newAccessToken = authHeader?.split(' ')[1]; - console.log(newAccessToken); - // setAuth(newAccessToken, response.data.user); - return response.data; + + // 토큰에서 사용자 정보 추출 + const decodedToken = jwtDecode(newAccessToken); + const user = { + userId: decodedToken.sub.userId, + nickname: decodedToken.sub.nickName, + role: decodedToken.sub.role, + }; + + console.log('Refresh token user:', user); + return { accessToken: newAccessToken, user }; } catch (error) { console.error('Failed to refresh token', error); // clearAuth(); @@ -130,6 +142,7 @@ export const guestLogin = async (nickname: string) => { const user = { userId: decodedToken.sub.userId, nickname: decodedToken.sub.nickName, + role: decodedToken.sub.role, }; useAuthStore.getState().setAuth(accessToken, user); diff --git a/src/services/socketService.ts b/src/services/socketService.ts index 39606e4..53eea31 100644 --- a/src/services/socketService.ts +++ b/src/services/socketService.ts @@ -55,11 +55,12 @@ class SocketService { } } // 픽셀 업데이트 수신 - onPixelUpdate(callback: (pixelData: PixelData) => void) { + OnPixelUpdate(callback: (data: { pixels: Array }) => void) { if (this.socket) { this.socket.on('pixel_update', callback); } } + // 쿨다운 정보 수신 onCooldownInfo( callback: (data: { cooldown: boolean; remaining: number }) => void @@ -198,7 +199,7 @@ class SocketService { // 게임 픽셀 업데이트 수신 onGamePixelUpdate(callback: (pixelData: PixelData) => void) { if (this.socket) { - this.socket.on('pixel_update', (data) => { + this.socket.on('game_pixel_update', (data) => { console.log( 'SocketService: Received pixel_update for game canvas', data diff --git a/src/store/authStrore.ts b/src/store/authStrore.ts index 5726966..e389374 100644 --- a/src/store/authStrore.ts +++ b/src/store/authStrore.ts @@ -7,6 +7,7 @@ type User = { userId: string; nickname?: string; email?: string; + role?: string; }; type AuthState = { @@ -25,7 +26,7 @@ export const useAuthStore = create((set) => ({ set({ isLoggedIn: true, accessToken: token, - user: userData, + user: userData, // userData now includes the role field }), clearAuth: () => set({ diff --git a/src/store/modalStore.ts b/src/store/modalStore.ts index 30e90ec..0de2232 100644 --- a/src/store/modalStore.ts +++ b/src/store/modalStore.ts @@ -11,6 +11,10 @@ type ModalState = { openCanvasModal: () => void; closeCanvasModal: () => void; + isGameModalOpen: boolean; + openGameModal: () => void; + closeGameModal: () => void; + isAlbumModalOpen: boolean; openAlbumModal: () => void; closeAlbumModal: () => void; @@ -44,6 +48,7 @@ export const useModalStore = create((set) => ({ isLoginModalOpen: true, isCanvasModalOpen: false, isAlbumModalOpen: false, + isGameModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: false, isChatOpen: false, @@ -58,6 +63,7 @@ export const useModalStore = create((set) => ({ isLoginModalOpen: false, isCanvasModalOpen: true, isAlbumModalOpen: false, + isGameModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: false, isChatOpen: false, @@ -66,11 +72,27 @@ export const useModalStore = create((set) => ({ }), closeCanvasModal: () => set({ isCanvasModalOpen: false }), + isGameModalOpen: false, + openGameModal: () => + set({ + isLoginModalOpen: false, + isCanvasModalOpen: false, + isGameModalOpen: true, + isAlbumModalOpen: false, + isMyPageModalOpen: false, + isGroupModalOpen: false, + isChatOpen: false, + isHelpModalOpen: false, + isCanvasEndedModalOpen: false, // 추가 + }), + closeGameModal: () => set({ isGameModalOpen: false }), + isAlbumModalOpen: false, openAlbumModal: () => set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: true, isMyPageModalOpen: false, isGroupModalOpen: false, @@ -85,6 +107,7 @@ export const useModalStore = create((set) => ({ set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: false, isMyPageModalOpen: true, isGroupModalOpen: false, @@ -99,6 +122,7 @@ export const useModalStore = create((set) => ({ set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: true, @@ -114,6 +138,7 @@ export const useModalStore = create((set) => ({ set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: false, @@ -129,6 +154,7 @@ export const useModalStore = create((set) => ({ set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: false, @@ -144,12 +170,13 @@ export const useModalStore = create((set) => ({ set({ isLoginModalOpen: false, isCanvasModalOpen: false, + isGameModalOpen: false, isAlbumModalOpen: false, isMyPageModalOpen: false, isGroupModalOpen: false, isChatOpen: false, isHelpModalOpen: false, - isCanvasEndedModalOpen: true + isCanvasEndedModalOpen: true, // 추가 }), closeCanvasEndedModal: () => set({ isCanvasEndedModalOpen: false }), }));