Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b77781d
fix: 코인토스 포인트 배팅시 서버가 느리다면 앞면 뒷면이 disabeld가 되지 않아 선택이 되는 문제 해결
Ethen1264 Apr 28, 2025
698bcec
fix: 값이 변경되거나 상위 컴포넌트가 리렌더링 될때 영상이 사라지는 문제 해결
Ethen1264 Apr 28, 2025
393a1d4
refector: coin-toss page
Ethen1264 Apr 28, 2025
f28fb3b
refector: 코인토스 애니매이션 코드 리팩토링
Ethen1264 Apr 28, 2025
96bfc2a
refector: YavarweePage 동작별로 분리
Ethen1264 Apr 28, 2025
5236181
refector: 야바위 애니매이션 코드 분리
Ethen1264 Apr 28, 2025
e1b2f0e
chore: 주석 제거
Ethen1264 Apr 28, 2025
5934b6a
refector: 컵 셔플 방식 기존의 방식으로 적용
Ethen1264 Apr 29, 2025
52fa00b
Merge branch 'develop' of https://github.com/team-gogo/GOGO-Frontend …
Ethen1264 Apr 29, 2025
6d3881f
fix: stage 옮길때 포인트 값 변하지 않는 이슈
gjaegyun Apr 29, 2025
5dce94f
fix: end 된 게임 후순위 배치
gjaegyun Apr 30, 2025
d4681e1
fix: isPending 상태에 따른 storage 값 삭제
gjaegyun Apr 30, 2025
9f8cbe3
Merge pull request #224 from team-gogo/fix/couponStorage
gjaegyun Apr 30, 2025
a883ac6
fix: detail match page의 date container 적용
gjaegyun Apr 30, 2025
d10766d
Merge pull request #227 from team-gogo/fix/detailMatch
gjaegyun May 1, 2025
3925fc7
Merge branch 'develop' into fix/stageMovePointIssue
gjaegyun May 1, 2025
831bdb9
Merge pull request #223 from team-gogo/fix/stageMovePointIssue
gjaegyun May 1, 2025
e49d8eb
Merge branch 'develop' into fix/matchEndSort
gjaegyun May 1, 2025
a75b0cf
Merge pull request #225 from team-gogo/fix/matchEndSort
gjaegyun May 1, 2025
a9fbc66
Merge branch 'develop' of https://github.com/team-gogo/GOGO-Frontend …
Ethen1264 May 1, 2025
d20727f
Merge pull request #221 from team-gogo/chore/mini-game
Ethen1264 May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 29 additions & 31 deletions src/entities/main/ui/DateContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DateContainer = () => {
}, []);
const pastDays = Math.floor(totalDays / 2);

const { setSelectDate } = useSelectDateStore();
const { selectDate, setSelectDate } = useSelectDateStore();

const { stageId } = useMyStageIdStore();

Expand All @@ -42,39 +42,37 @@ const DateContainer = () => {
return { short: `${month}-${day}`, full: `${year}-${month}-${day}` };
});

const syncSelectedDate = (fullDate: string) => {
const matchedDate = dates.find((d) => d.full === fullDate);

if (matchedDate) {
setSelectedDate(matchedDate.short);
setSelectDate(matchedDate.full);
setStartIndex(dates.findIndex((d) => d.full === fullDate));
} else {
const date = new Date(fullDate);
const short = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
setSelectedDate(short);
setSelectDate(fullDate);
setStartIndex(dates.findIndex((d) => d.full === fullDate));
}
};

useEffect(() => {
const localSelectDate = sessionStorage.getItem('selectDate');

if (localSelectDate) {
const { selectDate: localDate, stageId: localStageId } =
JSON.parse(localSelectDate);

if (localDate !== '') {
if (stageId === localStageId) {
const matchedDate = dates.find((d) => d.full === localDate);

if (matchedDate) {
setSelectedDate(matchedDate.short);
setSelectDate(matchedDate.full);
const matchedIndex = dates.findIndex((d) => d.full === localDate);
setStartIndex(matchedIndex);
} else {
const date = new Date(localDate);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const short = `${month}-${day}`;

setSelectedDate(short);
setSelectDate(localDate);
const matchedIndex = dates.findIndex((d) => d.full === localDate);
setStartIndex(matchedIndex);
}
}
}
if (selectDate) syncSelectedDate(selectDate);
}, []);

useEffect(() => {
const local = sessionStorage.getItem('selectDate');
if (!local) return;

const { selectDate: localDate, stageId: localStageId } = JSON.parse(local);
sessionStorage.removeItem('selectDate');

sessionStorage.removeItem('selectDate');
if (localDate && stageId === localStageId) {
syncSelectedDate(localDate);
}
}, [dates]);
}, [dates, stageId]);

const todayIndex = dates.findIndex(
(d) =>
Expand Down
22 changes: 22 additions & 0 deletions src/entities/mini-game/coin-toss/model/drawVideoToCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function drawVideoFrameToCanvas(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
) {
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const cropWidth = videoWidth;
const cropHeight = videoHeight;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;
const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);
}
80 changes: 80 additions & 0 deletions src/entities/mini-game/coin-toss/model/useVideoCanvasPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { useEffect, useRef } from 'react';
import { drawVideoFrameToCanvas } from './drawVideoToCanvas';

interface UseVideoCanvasPlayerProps {
videoSource: string;
isPlaying: boolean;
onAnimationEnd?: () => void;
}

export function useVideoCanvasPlayer({
videoSource,
isPlaying,
onAnimationEnd,
}: UseVideoCanvasPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

let isMounted = true;
const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawVideoFrames = () => {
if (!video || !canvas || !ctx) return;
if (video.readyState < 2) return;

drawVideoFrameToCanvas(video, canvas, ctx);

if (isMounted && isPlaying) {
animationFrameId.current = requestAnimationFrame(drawVideoFrames);
}
};

const drawInitialFrame = () => {
if (!video || !canvas || !ctx) return;
if (video.readyState < 2) return;

drawVideoFrameToCanvas(video, canvas, ctx);
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawVideoFrames();
} else {
drawInitialFrame();
}
};

const handleEnded = () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
onAnimationEnd?.();
};

video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('ended', handleEnded);

video.src = videoSource;
video.load();

return () => {
isMounted = false;
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('ended', handleEnded);
if (animationFrameId.current)
cancelAnimationFrame(animationFrameId.current);
};
}, [videoSource, isPlaying, onAnimationEnd]);

return { videoRef, canvasRef };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, RefObject, useRef } from 'react';

interface Args {
videoRef: RefObject<HTMLVideoElement>;
canvasRef: RefObject<HTMLCanvasElement>;
videoSource: string;
isPlaying: boolean;
onAnimationEnd?: () => void;
}

export function useVideoCanvasRenderer({
videoRef,
canvasRef,
videoSource,
isPlaying,
onAnimationEnd,
}: Args) {
const frameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

let mounted = true;
const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawLoop = () => {
if (!mounted || !isPlaying) return;
if (video.readyState < 2) {
frameId.current = requestAnimationFrame(drawLoop);
return;
}

const vw = video.videoWidth;
const vh = video.videoHeight;
const cw = canvas.width;
const ch = canvas.height;

ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(video, 0, vh - vh, vw, vh, 0, 0, cw, ch);

frameId.current = requestAnimationFrame(drawLoop);
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawLoop();
} else {
if (video.readyState >= 2) {
const vw = video.videoWidth;
const vh = video.videoHeight;
const cw = canvas.width;
const ch = canvas.height;

ctx.clearRect(0, 0, cw, ch);
ctx.drawImage(video, 0, vh - vh, vw, vh, 0, 0, cw, ch);
}
}
};

const handleEnded = () => {
if (frameId.current) cancelAnimationFrame(frameId.current);
onAnimationEnd?.();
};

video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('ended', handleEnded);

video.src = videoSource;
video.load();

return () => {
mounted = false;
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('ended', handleEnded);
if (frameId.current) cancelAnimationFrame(frameId.current);
};
}, [videoSource, isPlaying, onAnimationEnd, videoRef, canvasRef]);
}
97 changes: 7 additions & 90 deletions src/entities/mini-game/coin-toss/ui/CoinTossAnimation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client';

import { useEffect, useRef } from 'react';
import React from 'react';
import { cn } from '@/shared/utils/cn';
import { useVideoCanvasPlayer } from '../../model/useVideoCanvasPlayer';

interface CoinTossAnimationProps {
isPlaying: boolean;
Expand All @@ -14,94 +15,11 @@ const CoinTossAnimation = ({
videoSource,
onAnimationEnd,
}: CoinTossAnimationProps) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameId = useRef<number>();

useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;

const ctx = canvas.getContext('2d');
if (!ctx) return;

const drawToCanvas = () => {
if (!video || !canvas || !ctx) return;

const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const zoom = 1;
const cropWidth = videoWidth / zoom;
const cropHeight = videoHeight / zoom;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;

const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);

animationFrameId.current = requestAnimationFrame(drawToCanvas);
};

const drawInitialFrame = () => {
if (!video || !canvas || !ctx) return;

const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const zoom = 1;
const cropWidth = videoWidth / zoom;
const cropHeight = videoHeight / zoom;
const centerX = videoWidth * 0.5;
const centerY = videoHeight * 1;

const sx = Math.max(0, centerX - cropWidth / 2);
const sy = Math.max(0, centerY - cropHeight / 2);
const sw = Math.min(cropWidth, videoWidth - sx);
const sh = Math.min(cropHeight, videoHeight - sy);

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);
};

const handleEnded = () => {
if (animationFrameId.current) {
cancelAnimationFrame(animationFrameId.current);
}
onAnimationEnd?.();
};

const handleLoadedData = () => {
if (isPlaying) {
video.play();
drawToCanvas();
} else {
drawInitialFrame();
}
};

video.addEventListener('ended', handleEnded);
video.addEventListener('loadeddata', handleLoadedData);

// ✅ 무조건 load 호출해서 첫 프레임도 준비되게
video.load();

return () => {
video.removeEventListener('ended', handleEnded);
video.removeEventListener('loadeddata', handleLoadedData);
cancelAnimationFrame(animationFrameId.current!);
};
}, [isPlaying, videoSource, onAnimationEnd]);
const { videoRef, canvasRef } = useVideoCanvasPlayer({
videoSource,
isPlaying,
onAnimationEnd,
});

return (
<div className={cn('relative', 'rounded-lg')}>
Expand All @@ -120,7 +38,6 @@ const CoinTossAnimation = ({
<video
ref={videoRef}
className="hidden"
src={videoSource}
muted
playsInline
preload="auto"
Expand Down
Loading