From 0627f834a089a29620ba09b50d4dd69d69c9beb0 Mon Sep 17 00:00:00 2001 From: Anas Date: Wed, 9 Jul 2025 11:08:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[SCRUM-141]:=EC=B1=84=ED=8C=85=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80(x,y)=20=EC=9E=85=EB=A0=A5=EC=8B=9C=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=ED=94=BD=EC=85=80=EB=A1=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/canvas/PixelCanvas.test.tsx | 14 ++++ src/components/canvas/PixelCanvas.tsx | 76 ++++++++++++++++++++++ src/components/chat/MessageItem.tsx | 45 ++++++++++++- src/setupTests.ts | 7 ++ src/store/canvasUiStore.ts | 6 ++ vitest.config.ts | 11 ++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/components/canvas/PixelCanvas.test.tsx create mode 100644 src/setupTests.ts create mode 100644 vitest.config.ts diff --git a/src/components/canvas/PixelCanvas.test.tsx b/src/components/canvas/PixelCanvas.test.tsx new file mode 100644 index 0000000..49fc44e --- /dev/null +++ b/src/components/canvas/PixelCanvas.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react'; +import PixelCanvas from './PixelCanvas'; + +describe('PixelCanvas', () => { + it('should measure rendering time', () => { + const startTime = performance.now(); + render(); + const endTime = performance.now(); + const renderTime = endTime - startTime; + console.log(`PixelCanvas rendering time: ${renderTime} ms`); + // You can add an assertion here if you have a specific performance target + // expect(renderTime).toBeLessThan(100); // Example: expect rendering to be less than 100ms + }); +}); diff --git a/src/components/canvas/PixelCanvas.tsx b/src/components/canvas/PixelCanvas.tsx index 9ed66de..7037ed5 100644 --- a/src/components/canvas/PixelCanvas.tsx +++ b/src/components/canvas/PixelCanvas.tsx @@ -94,6 +94,8 @@ function PixelCanvas({ const setHasError = useCanvasUiStore((state) => state.setHasError); const showCanvas = useCanvasUiStore((state) => state.showCanvas); const setShowCanvas = useCanvasUiStore((state) => state.setShowCanvas); + const targetPixel = useCanvasUiStore((state) => state.targetPixel); + const setTargetPixel = useCanvasUiStore((state) => state.setTargetPixel); const startCooldown = useCanvasUiStore((state) => state.startCooldown); @@ -584,6 +586,71 @@ function PixelCanvas({ zoomCanvas(1 / 1.2); }, [zoomCanvas]); + const centerOnWorldPixel = useCallback( + (worldX: number, worldY: number) => { + const canvas = renderCanvasRef.current; + if (!canvas) return; + + // Check if the target pixel is within canvas bounds + if ( + worldX < 0 || + worldX >= canvasSize.width || + worldY < 0 || + worldY >= canvasSize.height + ) { + console.warn( + `Target pixel (${worldX}, ${worldY}) is out of canvas bounds.` + ); + return; + } + + const viewportCenterX = canvas.clientWidth / 2; + const viewportCenterY = canvas.clientHeight / 2; + + // Calculate the target view position to center the world pixel + // (worldX + 0.5) to center on the pixel, not its top-left corner + const targetX = viewportCenterX - (worldX + 0.5) * scaleRef.current; + const targetY = viewportCenterY - (worldY + 0.5) * scaleRef.current; + + const startX = viewPosRef.current.x; + const startY = viewPosRef.current.y; + const duration = 1000; // Animation duration in ms + const startTime = performance.now(); + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + // Ease-out cubic function + const eased = 1 - Math.pow(1 - progress, 3); + + viewPosRef.current.x = startX + (targetX - startX) * eased; + viewPosRef.current.y = startY + (targetY - startY) * eased; + + draw(); + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + // Ensure final position is exact + viewPosRef.current.x = targetX; + viewPosRef.current.y = targetY; + draw(); + + // Set fixedPosRef to highlight the target pixel + fixedPosRef.current = { x: worldX, y: worldY, color: 'transparent' }; // Use transparent or a default color + draw(); // Redraw to show the fixedPosRef + + // Optionally, update overlay for the centered pixel + const screenX = worldX * scaleRef.current + viewPosRef.current.x; + const screenY = worldY * scaleRef.current + viewPosRef.current.y; + updateOverlay(screenX, screenY); + } + }; + requestAnimationFrame(animate); + }, + [draw, canvasSize, updateOverlay] + ); + const handleCooltime = useCallback(() => { startCooldown(10); }, [startCooldown]); @@ -972,6 +1039,15 @@ function PixelCanvas({ } }, [imageTransparency, draw]); + // Listen for targetPixel changes from chat and center the canvas + useEffect(() => { + if (targetPixel) { + centerOnWorldPixel(targetPixel.x, targetPixel.y); + // Reset targetPixel to null after processing to prevent re-triggering + setTargetPixel(null); + } + }, [targetPixel, centerOnWorldPixel, setTargetPixel]); + useEffect(() => { const rootElement = rootRef.current; if (!rootElement) return; diff --git a/src/components/chat/MessageItem.tsx b/src/components/chat/MessageItem.tsx index 3d3742a..3e9b413 100644 --- a/src/components/chat/MessageItem.tsx +++ b/src/components/chat/MessageItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useAuthStore } from '../../store/authStrore'; +import { useCanvasUiStore } from '../../store/canvasUiStore'; export type Message = { messageId: string; @@ -13,8 +14,50 @@ export type Message = { const MessageItem = React.memo(({ message }: { message: Message }) => { const currentUser = useAuthStore((state) => state.user); + const setTargetPixel = useCanvasUiStore((state) => state.setTargetPixel); const isMyMessage = message.user.userId === currentUser?.userId; + const handleCoordinateClick = (x: number, y: number) => { + setTargetPixel({ x, y }); + }; + + const renderMessageContent = (content: string) => { + const parts: React.ReactNode[] = []; + const regex = /\((\d+),(\d+)\)/g; + let lastIndex = 0; + let match; + + while ((match = regex.exec(content)) !== null) { + const [fullMatch, xStr, yStr] = match; + const x = parseInt(xStr, 10); + const y = parseInt(yStr, 10); + + // Add the text before the coordinate + if (match.index > lastIndex) { + parts.push(content.substring(lastIndex, match.index)); + } + + // Add the clickable coordinate + parts.push( + handleCoordinateClick(x, y)} + > + {fullMatch} + + ); + lastIndex = regex.lastIndex; + } + + // Add any remaining text after the last coordinate + if (lastIndex < content.length) { + parts.push(content.substring(lastIndex)); + } + + return parts; + }; + console.log(message.user.userId); const messageBubbleClasses = isMyMessage ? 'bg-blue-500 text-white rounded-lg py-2 px-3 max-w-[70%] self-end' @@ -32,7 +75,7 @@ const MessageItem = React.memo(({ message }: { message: Message }) => { )}
-
{message.content}
+
{renderMessageContent(message.content)}
{message.timestamp && (
void; + // 채팅에서 클릭된 좌표 + targetPixel: { x: number; y: number } | null; + setTargetPixel: (pos: { x: number; y: number } | null) => void; // 잔여 쿨다운 시간 timeLeft: number; setTimeLeft: (timeLeft: number | ((prev: number) => number)) => void; @@ -44,6 +47,9 @@ export const useCanvasUiStore = create((set, get) => ({ hoverPos: null, setHoverPos: (hoverPos) => set({ hoverPos }), + targetPixel: null, + setTargetPixel: (targetPixel) => set({ targetPixel }), + timeLeft: 0, setTimeLeft: (newTimeLeft) => set((state) => ({ diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fc38606 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/setupTests.ts'], + }, +}); From e066e13ef81ae73c080f700c2dcaef6f92efa4d2 Mon Sep 17 00:00:00 2001 From: yoominlee00 Date: Wed, 9 Jul 2025 14:26:04 +0900 Subject: [PATCH 2/3] SCRUM-142: canvas-movement-refresh --- src/components/modal/CanvasModalContent.tsx | 4 ++-- src/services/socketService.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/modal/CanvasModalContent.tsx b/src/components/modal/CanvasModalContent.tsx index 0b774a7..a2e10f0 100644 --- a/src/components/modal/CanvasModalContent.tsx +++ b/src/components/modal/CanvasModalContent.tsx @@ -230,8 +230,8 @@ const CanvasModalContent = ({ onClose }: CanvasModalContentProps) => { onClose(); } - // 2. 페이지 이동 (새로고침 없음) - navigate(getCanvasUrl(canvasId)); + // 2. 페이지 이동 (새로고침 포함) + window.location.href = getCanvasUrl(canvasId); }; // URL 생성 함수 (Query parameter 방식 사용) diff --git a/src/services/socketService.ts b/src/services/socketService.ts index cc84717..a395ab0 100644 --- a/src/services/socketService.ts +++ b/src/services/socketService.ts @@ -137,7 +137,7 @@ class SocketService { // 인증 에러 리스너 제거 offAuthError(callback: (error: { message: string }) => void) { if (this.socket) { - this.socket.off('autherror', callback); + this.socket.off('auth_error', callback); } } } From dde065c7616e62498236ead58a21c65e2a8321ae Mon Sep 17 00:00:00 2001 From: Anas Date: Wed, 9 Jul 2025 14:38:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test=20=ED=8C=8C=EC=9D=BC=20Gitignore=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=EC=9C=BC=EB=A1=9C=20=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + index.html | 2 +- src/components/canvas/PixelCanvas.test.tsx | 14 -------------- 3 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 src/components/canvas/PixelCanvas.test.tsx diff --git a/.gitignore b/.gitignore index 50c8dda..1a30e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? .env +test \ No newline at end of file diff --git a/index.html b/index.html index 12d2898..7f6b79f 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Pick-Px
diff --git a/src/components/canvas/PixelCanvas.test.tsx b/src/components/canvas/PixelCanvas.test.tsx deleted file mode 100644 index 49fc44e..0000000 --- a/src/components/canvas/PixelCanvas.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { render } from '@testing-library/react'; -import PixelCanvas from './PixelCanvas'; - -describe('PixelCanvas', () => { - it('should measure rendering time', () => { - const startTime = performance.now(); - render(); - const endTime = performance.now(); - const renderTime = endTime - startTime; - console.log(`PixelCanvas rendering time: ${renderTime} ms`); - // You can add an assertion here if you have a specific performance target - // expect(renderTime).toBeLessThan(100); // Example: expect rendering to be less than 100ms - }); -});