Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3주차 기본/심화/공유 과제] 1 to 50 게임 #6

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion README.md

This file was deleted.

13 changes: 13 additions & 0 deletions week3_practice/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Click the Numbers Game</title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions week3_practice/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "week3_practice",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.13",
"globals": "^15.11.0",
"vite": "^5.4.10"
}
}
1 change: 1 addition & 0 deletions week3_practice/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions week3_practice/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.app {
text-align: center;
margin-top: 80px;
}

.tab-container {
display: flex;
justify-content: center;
margin-top: 20px;
}

.tab-container button {
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
margin: 0 5px;
border: none;
border-radius: 5px;
}

.tab-container .active {
background-color: #4caf50;
color: white;
}

.next-number {
font-weight: bold;
padding: 30px;
}
98 changes: 98 additions & 0 deletions week3_practice/src/app.jsx
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 파일명이 app.jsx로 되어있는데요!
일반적으로 React 컴포넌트 파일 이름은 대문자로 시작해서 파스칼 케이스에 맞게 작성합니다!!
크게 문제가 되는 것은 아니지만, App.jsx로 이름을 지어서 컴포넌트라는 것을 쉽게 알 수 있도록 하는 것을 추천드립니다 :)

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// src/App.jsx

import React, { useState, useRef, useEffect } from 'react';
import Header from './components/Header';
import Board from './components/Board';
import RankingBoard from './components/RankingBoard';
import './App.css';

const App = () => {
const [selectedTab, setSelectedTab] = useState('game'); // 'game' 또는 'ranking'
Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호! 이렇게 주석으로 어떤 어떤 탭이 있는지 설명해주는 것 너무 좋은 것 같습니다 ㅎㅎ

const [isGameStarted, setIsGameStarted] = useState(false);
const [isGameCompleted, setIsGameCompleted] = useState(false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 여기 isGameCompleted 상태는 무슨 역할을 하는 상태인가요?? 이 상태값을 사용하는 로직이 없는 것 같아서요!!

const [time, setTime] = useState(0);
const [nextNumber, setNextNumber] = useState(1);
const timerRef = useRef(null);
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef를 사용해서 불필요한 리렌더링을 방지하는 코드 너무 좋은 것 같습니다!! 최고최고👍🏻👍🏻

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setInterval의 리렌더링만 방지하고 있어서 타이머의 상태가 바뀔 때 발생하는 리렌더링은 방지하지 못하고 있어요...! 이 부분도 방지하면 더 좋을 것 같습니다 :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㄷ ㄷ 레전드 개발자 탄생


useEffect(() => {
if (isGameStarted) {
timerRef.current = setInterval(() => {
setTime((prevTime) => prevTime + 0.01);
}, 10);
} else {
clearInterval(timerRef.current); // 타이머 정지
}


return () => clearInterval(timerRef.current);
}, [isGameStarted]);

const startGame = () => {
setTime(0); // 타이머 초기화
setNextNumber(1);
setIsGameStarted(false);
setIsGameCompleted(false);
};

Comment on lines +29 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수는 선언만 되어있고 호출은 안하고 있는데 선언하신 궁금합니다!!
상태들을 초기화하고 있는데 이미 위에서 처음에 초기화해준 것이랑 같기 때문에 작성하신 의도도 궁금합니다!ㅎㅎ

const startTimerOnFirstClick = () => {
if (!isGameStarted) {
setIsGameStarted(true); // 첫 숫자를 누를 때 타이머 시작
}
};

const completeGame = () => {
setIsGameStarted(false);
setIsGameCompleted(true);

// 게임 완료 시 로컬 스토리지에 기록 저장
const gameData = {
date: new Date().toLocaleString(),
level: "레벨 1",
playTime: time.toFixed(2),
};
localStorage.setItem(`gameResult_${Date.now()}`, JSON.stringify(gameData));

alert(`성공! ${time.toFixed(2)}초 소요`);
};

const resetGame = () => {
setIsGameStarted(false);
setTime(0);
setNextNumber(1);
setIsGameCompleted(false);
};

const handleTabChange = (tab) => {
resetGame();
setSelectedTab(tab);
};

return (
<>
<Header
selectedTab={selectedTab}
onTabChange={handleTabChange}
time={time}
isGameStarted={isGameStarted}
/>
<div className="app">
{selectedTab === 'game' && (
<div className="game-container">
<div className="next-number">다음 숫자: {nextNumber}</div>
<Board
nextNumber={nextNumber}
onCorrectClick={() => {
startTimerOnFirstClick(); // 첫 숫자 클릭 시 타이머 시작
setNextNumber((prev) => prev + 1);
}}
onGameComplete={completeGame}
/>
</div>
)}
{selectedTab === 'ranking' && <RankingBoard />}
</div>
</>
);
};

export default App;
1 change: 1 addition & 0 deletions week3_practice/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions week3_practice/src/components/Board.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.board {
display: grid;
gap: 10px;
grid-template-columns: repeat(3, 80px);
grid-template-rows: repeat(3, 80px);
justify-content: center;
}

48 changes: 48 additions & 0 deletions week3_practice/src/components/Board.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState, useEffect } from 'react';
import Tile from './Tile';
import './Board.css';

const Board = ({ nextNumber, onCorrectClick, onGameComplete }) => {
const [numbers, setNumbers] = useState([]);

useEffect(() => {
initializeBoard();
}, []);

const initializeBoard = () => {
const initialNumbers = Array.from({ length: 9 }, (_, i) => i + 1); // 1 to 9
shuffleArray(initialNumbers);
setNumbers(initialNumbers);
};

const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
Comment on lines +12 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shuffleArray에서 initialNumbers라는 배열의 값을 직접 변경해주면서 불변성이 지켜지지 않고 있습니다..!

React에서는 불변성을 지키는 것이 중요합니다.
물론 이 코드에서는 원본 배열을 다시 사용하지 않기 때문에 문제가 발생하지 않지만, 만약 원본 배열이 다른 곳에서도 사용되고 있다고 가정한다면 직접 값을 바꾸면 다른 곳에도 영향을 주기 때문에 예상치못한 문제가 발생할 수 있습니다.
참고하면 좋은 블로그 같이 첨부드립니다.

따라서 아래처럼 코드를 수정해서 불변성을 지키는 것은 어떨까요??ㅎㅎ

Suggested change
const initializeBoard = () => {
const initialNumbers = Array.from({ length: 9 }, (_, i) => i + 1); // 1 to 9
shuffleArray(initialNumbers);
setNumbers(initialNumbers);
};
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
const initializeBoard = () => {
const initialNumbers = Array.from({ length: 9 }, (_, i) => i + 1); // 1 to 9
const shuffledNumbers = shuffleArray(initialNumbers);
setNumbers(shuffledNumbers);
};
const shuffleArray = (array) => {
const newArray = [...array]; // 원본 배열 복사
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 리액트 첫 과제 했을때 불변성에 대해 코드리뷰 받았었는데, 온전히 이해하기 어려웠던 기억이 새록새록하네요,,!💜🫧
앞으로 계속 신경써야하는 부분이라 참고된 아티클 정독 추천합니다!!


const handleTileClick = (number) => {
if (number === nextNumber) {
if (number === 9) { // 9가 마지막 숫자
onGameComplete();
} else {
onCorrectClick();
}
}
};

return (
<div className="board">
{numbers.map((number) => (
<Tile
key={number}
number={number}
onClick={() => handleTileClick(number)}
/>
))}
</div>
);
};

export default Board;
39 changes: 39 additions & 0 deletions week3_practice/src/components/Header.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #4caf50;
color: white;
position: fixed;
top: 0;
left: 0;
width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}

.tabs {
display: flex;
gap: 10px;
}

.tabs button {
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
background: none;
border: none;
color: white;
}

.tabs .active {
background-color: white;
color: #4caf50;
border-radius: 5px;
}

.header-right {
display: flex;
gap: 10px;
}

36 changes: 36 additions & 0 deletions week3_practice/src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import './Header.css';

const Header = ({ selectedTab, onTabChange, time }) => {
return (
<header className="header">
<span>1 to 50</span>
<div className="tabs">
<button
onClick={() => onTabChange('game')}
className={selectedTab === 'game' ? 'active' : ''}
>
게임
</button>
<button
onClick={() => onTabChange('ranking')}
className={selectedTab === 'ranking' ? 'active' : ''}
>
랭킹
</button>
</div>
{selectedTab === 'game' && (
<div className="header-right">
<select>
<option>레벨 1</option>
<option>레벨 2</option>
<option>레벨 3</option>
</select>
<span>타이머: {time.toFixed(2)} 초</span>
</div>
)}
</header>
);
};

export default Header;
52 changes: 52 additions & 0 deletions week3_practice/src/components/RankingBoard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.ranking-board {
margin-top: 20px;
padding: 20px;
background-color: #ffffff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}

.ranking-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.reset-button {
padding: 5px 10px;
font-size: 0.9em;
color: #4caf50;
background-color: #fff;
border: 1px solid #4caf50;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
}

.reset-button:hover {
background-color: #4caf50;
color: white;
}

.ranking-board table {
width: 100%;
border-collapse: collapse;
}

.ranking-board th,
.ranking-board td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}

.ranking-board th {
background-color: #4caf50;
color: white;
}

.ranking-board td {
color: #555;
}

Loading