[로데시] 개발

[대학전쟁3] 양면빙고 웹 개발 07 - timer, highlight, restart 본문

개인 개발/대학전쟁

[대학전쟁3] 양면빙고 웹 개발 07 - timer, highlight, restart

로데시 2026. 1. 31. 20:16
반응형

 

 


 

0️⃣ 지난 번까지..

지난 주에 양면빙고를 끝냈어야 했는데... 너무 오랜만에 글을 써본다...
 
지난 글에서는 빙고판 위의 타일 개수에 따라 이동 뒤집기를 제한하는 기능을 구현했다.
 
오늘은 이어서 시간 제한을 만들어 제한시간이 넘어가면 turn이 넘어가는 기능을 구현해보고자 한다.
 

 

 

 

 

1️⃣ timer

🌟 App.js 코드
 
📌 const 추가
 
const TURN_LIMIT = 60;
const [timeLeft, setTimeLeft] = useState(TURN_LIMIT);
const isDanger = timeLeft <= 5;

ㆍ TURN_LIMIT - 한 turn당 제한 시간을 60으로(초 단위) 설정한다. (현재는 test 단계기 때문에 10으로 설정했다)
 
ㆍ timeLeft - 남은 시간을 표시하는데 사용한다.
 
ㆍ isDanger - timeLeft가 5 초과일 경우 false, 5 이하로 줄어들면 true로 설정하여 시각적으로 서로 다른 알림을 주는데에 사용한다.
 

 
📌 endTurnNoCount 함수 추가
 
const endTurnNoCount = () => {
  if (!winner) {
    setCurrentTurn(prev => prev === "me" ? "opponent" : "me");
  }
};

현재는 다른 player의 turn으로 넘어가면 setTurnCount이 1 증가하게 되어있다. 하지만 시간 초과로 turn이 넘어갈 경우에는 setTurnCount이 증가하면 안 된다. (착수하지 않았기 때문이다.) 따라서 turn이 넘어가지만 setTurnCount은 증가하지 않는 함수, endTurnNoCount를 추가로 작성한다.
 

 
📌 useEffect 작성
 
useEffect(() => {
  setTimeLeft(TURN_LIMIT);
}, [currentTurn]);

턴 바뀔 때마다 타이머 TURN_LIMIT가 reset 되도록 코드를 작성한다.
 

 
📌 useEffect 작성
 
useEffect(() => {
  if (winner) return;

  const timer = setInterval(() => {
    setTimeLeft(prev => {
      if (prev <= 1) {
        endTurnNoCount();
        return TURN_LIMIT;
      }
      return prev - 1;
    });
  }, 1000);

  return () => clearInterval(timer);
}, [currentTurn, winner]);

currentTurn이 변경될 때마다 타이머를 실행하기 위해 useEffect가 실행 되도록 한다. (단, winner가 결정되면 타이머를 실행하지 않도록 바로 return 한다.) setInterval을 통해 1초마다 setTimeLeft를 호출하여 남은 시간을 1씩 감소시킨다. 남은 시간이 1 이하가 되면 endTurnNoCount 함수를 호출해 setTurnCount이 증가 없이 턴을 종료하고 다음 player의 턴을 위해 시간을 TURN_LIMIT으로 초기화한다.
 

 
📌 return 수정
 
<div className={`timer ${isDanger ? "danger" : ""}`}>
  <span className="timer-icon">?</span>
  <span className="timer-text">Time Left</span>
  <span className="timer-value">{timeLeft}s</span>
</div>

남은 시간을 화면 상단에 표시한다. danger 클래스를 추가해 isDanger의 값에 따라 타이머의 시각적 효과를 달리 한다.
 

 

 
🌟 App.css 코드
 
📌 css 작성
 
.timer {
  display: inline-flex;
  align-items: center;
  gap: 8px;

  padding: 8px 14px;
  border-radius: 10px;

  background: #1f2937;
  color: #ffffff;

  font-weight: 600;

  margin-left: 300px;
}

.timer-icon {
  font-size: 18px;
}

.timer-value {
  font-size: 18px;
  font-weight: 700;
  color: #22c55e;
}

.timer.danger {
  background: #dc2626;
  animation: pulse 1s infinite;
}

.timer.danger .timer-value {
  color: #fca5a5;
}

@keyframes pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.1); }
  100% { transform: scale(1); }
}

.timer-container.danger {
  animation: pulse 1s infinite;
}

남은 시간이 5 초과일 경우에는 흰색, 초록색 text와 어두운 계열 배경으로 깔끔한 timer 버튼 UI를 구성한다. 남은 시간이 5 이하일 경우 글자색과 배경색이 각각 #fca5a5 , #dc2626 으로 변한다. 또한 pulse 애니메이션을 적용해 타이머의 크기가 반복적으로 커졌다 작아지도록 설정하여 시간이 얼마 남지 않았음을 시각적으로 강조한다.
 

 
📌 타이머
 
타이머
 
남은 시간이 5 이하일 때와 5 초과일 때 타이머가 서로 다른 스타일로 표시되는 것을 확인할 수 있다.
 

 

 

2️⃣ 빙고 완료 시, 빙고 highlight

🌟 App.js 코드
 
📌 const 추가
 
const [winningLine, setWinningLine] = useState([]);

완성된 빙고 line을 저장할 const를 작성한다.
 

 
📌 checkBingo 수정
 
// 빙고 기준 set
const lines = [
// 행 빙고
[0,1,2,3], [4,5,6,7], [8,9,10,11], [12,13,14,15],
// 열 빙고
[0,4,8,12], [1,5,9,13], [2,6,10,14], [3,7,11,15],
// 대각선 빙고
[0,5,10,15], [3,6,9,12],
];

const checkBingo = (boardTiles, targetColor) => {
  const winningLine = lines.find((line) =>
    line.every((idx) => {
      const tile = boardTiles[idx];
      if (!tile) return false;
      return getVisibleColor(tile) === targetColor;
    })
  );

  return winningLine || null;
};

빙고가 완성이 될 경우 해당 빙고를 시각적으로 표시하기 위해 checkBingo 함수 코드를 재작성한다. 빙고의 완성 여부를 판별함과 동시에 완성된 빙고 line 배열을 반환한다. 미리 정의한 'lines' 배열을 순회하며 빙고 조건에 만족하는 첫 번째 line을 반환한다.
 

 
📌 useEffect 수정
 
useEffect(() => {
  // 1. 내 빙고 체크
  const myBingoLine = checkBingo(boardTiles, players.me.mainColor);
  if (myBingoLine) {
    setWinner("me");
    setWinningLine(myBingoLine);
    return;
  }

  // 2. 상대 빙고 체크
  const oppBingoLine = checkBingo(boardTiles, players.opponent.mainColor);
  if (oppBingoLine) {
    setWinner("opponent");
    setWinningLine(oppBingoLine);
  }
}, [boardTiles]);

빙고판(bingoboard)이 바뀔 때마다 useEffect를 통해 checkBingo 호출한다. checkBingo를 통해 빙고가 완성됨을 확인하면 완성된 빙고 line을 setWinningLine에 저장한다.
 

 
📌 return 함수 수정
 
<BingoBoard winningLine={winningLine} />

완성된 빙고 line을 BingoBoard 에 전달해준다.
 

 

 
🌟 BingoBoard.js 코드
 
📌 코드 작성
 
const BingoCell = ({ winningLine= [] })
const isBingo = winningLine.includes(cellId);
isBingo={isBingo}

ㆍ winningLine= [] - winningLine을 props로 받는다.
 
ㆍ isBingo - 빙고가 완성된 cellId를 저장한다.
 
ㆍ isBingo={isBingo} - BingoCell에 빙고 여부 전달
 

 

 
🌟 BingoCell.js 코드
 
📌 코드 작성
 
const BingoCell = ({ isBingo })
${isBingo ? "bingo-highlight" : ""}

ㆍ isBingo - props로 받는다.
 
ㆍ isBingo ? "bingo-highlight" : "" - 빙고 완성 여부에 따른 CSS를 달리한다.
 

 
🌟 BingoBoard.css 코드
 
📌 css 작성
 
.bingo-highlight {
  border: 3px solid #11ff00 !important;
  box-shadow: 0 0 15px #11ff00;
  transform: scale(1.05);
  z-index: 10;
  transition: all 0.3s ease;
  animation: pulse 1s infinite;
}

@keyframes pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.05); }
  100% { transform: scale(1); }
}

빙고가 완성된 경우 border와 box-shadow를 사용해 완성된 line을 시각적으로 강조하고 pulse 애니메이션으로 highlight의 크기가 반복적으로 변하도록 스타일을 적용한다.
 

 
📌 highlight
 
highlight
 
빙고가 완성된 경우 초록색 highlight가 표시됨을 볼 수 있고 타일과 line의 크기가 반복적으로 변하는 것을 확인할 수 있다.
 

 

 

3️⃣ restart 버튼

🌟 App.js 코드
 
📌 disabled
 
{winner && (
  <div className="end">
    <h1 className="winner">
      Winner: {winner === "me" ? "ME" : "OPPONENT"}
    </h1>

    <button
      className="restart"
      onClick={() => window.location.reload()}
    >
      게임 다시 시작
    </button>
  </div>
)}

빙고가 완성되어 winner가 set되면 조건부 렌더링을 통해 winner를 표시하고 그 아래에는 게임을 다시 시작할 수 있는 restart 버튼을 렌더링한다. restart 버튼은 window.location.reload()를 호출해 웹 전체 상태를 초기화하기 때문에 새 게임을 이어서 할 수 있다.
 

 

 
🌟 App.css 코드
 
📌 css 작성
 
/* 빙고 완성 시 */
.end {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  color: white;

  display: flex;
  flex-direction: column; /* ? 세로 정렬 */
  align-items: center;
  justify-content: center;
  gap: 24px; /* h1과 버튼 사이 간격 */

  z-index: 999;
}

/* 승자 */
.winner {
  font-size: 48px;
  margin: 0;
}

/* 다시 시작 버튼 */
.restart {
  font-size: 18px;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  cursor: pointer;

  background-color: #ffffff;
  color: #000000;
}

.restart:hover {
  background-color: #e0e0e0;
}

기본 상태에서는 흰색 배경과 검은색 text로 깔끔한 restart 버튼 UI를 구성하고 마우스를 올렸을 때는 :hover를 통해 배경색이 회색으로 변경되도록 설정해 버튼이 클릭 가능한 인터랙션 요소임을 직관적으로 알 수 있도록 한다.
 

 

 
📌 restart
 
restart
 
빙고가 완성되면 restart 버튼이 화면에 표시되고 버튼을 클릭해 게임을 다시 시작할 수 있다.
 

 

 

4️⃣ 앞으로...

타이머, 빙고 highlight, restart 버튼을 만들었다. 이제 진짜 진짜 거의 끝이다! 다음에는 컴퓨터 한 대에서 턴을 번갈아 진행하던 방식이 아니라 서로 다른 두 기기에서 play할 수 있도록 구현하고자 한다. 또한 현재처럼 하나의 기기에서도 2명이 play할 수 있게 main color 등이 표시되는 방법을 바꿀 것이다. 그리고 웹 배포까지 하면 끝이다..
 

 

 

 

 

반응형