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 게임 #4

Open
wants to merge 18 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
24 changes: 24 additions & 0 deletions week3/number-game/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
8 changes: 8 additions & 0 deletions week3/number-game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
39 changes: 39 additions & 0 deletions week3/number-game/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import js from "@eslint/js";
import globals from "globals";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";

export default [
{ ignores: ["dist"] },
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: "module",
},
},
settings: { react: { version: "18.3" } },
plugins: {
react,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react/prop-types": "off",
},
},
];
14 changes: 14 additions & 0 deletions week3/number-game/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Number Game</title>
</head>
<body>
<div id="root"></div>
<div id="modal-root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions week3/number-game/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "number-game",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"styled-components": "^6.1.13"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"vite": "^5.4.10"
}
}
15 changes: 15 additions & 0 deletions week3/number-game/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import GlobalStyle from "../styles/GlobalStyle";
import { ThemeProvider } from "styled-components";
import theme from "../styles/theme";
import Router from "./Router";

function App() {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Router />
</ThemeProvider>
);
}

export default App;
14 changes: 14 additions & 0 deletions week3/number-game/src/Router.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Home } from "./pages/Home";

function Router() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
);
}

export default Router;
167 changes: 167 additions & 0 deletions week3/number-game/src/components/Home/GameBoard/GameBoard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { useState } from "react";
import styled, { keyframes, css } from "styled-components";
import CompleteModal from "../../Modal/CompleteModal";
import NextNumber from "./NextNumber";
import { generateShuffledNumbers } from "../../../utils/generateShuffledNumbers";
import { resetGame } from "../../../utils/resetGame";
import { handleNumberClick } from "../../../utils/handleNumberClick";

const GameBoard = ({
initialLength,
remainingLength,
gridLength,
Copy link
Member

Choose a reason for hiding this comment

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

p3:
gameSelectLevel로 아예 레벨 선택 로직을 유틸로 분리해두신 게 흥미롭네요,,!!
유틸 활용을 잘 하고 계셔서 코드가 더 재사용성이 높은 것 같아요!! 배워갑니다 ㅎㅎ

onFirstClick,
onLastClick,
onReset,
currentTime,
}) => {
const [initialNums, setInitialnums] = useState(() =>
generateShuffledNumbers(1, initialLength)
);
const [upcomingNums, setUpcomingNums] = useState(() =>
generateShuffledNumbers(initialLength + 1, remainingLength)
);
const [currentNumber, setCurrentNumber] = useState(1);
const [gameComplete, setGameComplete] = useState(false);
const [clickedIndexes, setClickedIndexes] = useState(new Set());

const handleInitialNumClick = (index) => {
const [next, ...rest] = upcomingNums;
setUpcomingNums(rest);
setInitialnums((prev) => prev.map((num, i) => (i === index ? next : num)));
setClickedIndexes((prev) => new Set(prev).add(index));
setCurrentNumber((prev) => prev + 1);
};

const handleUpcomingNumClick = (index) => {
setInitialnums((prev) => prev.map((num, i) => (i === index ? null : num)));
setCurrentNumber((prev) => prev + 1);
};

const reset = () => {
resetGame(initialLength, remainingLength, {
setInitialnums,
setUpcomingNums,
setCurrentNumber,
setGameComplete,
setClickedIndexes,
onReset,
});
};
Comment on lines +41 to +50

Choose a reason for hiding this comment

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

p3: resetGame utils을 분리를 잘 하셨는데 이를 직접 사용하는 게 아니라 reset에 감싸서 사용하는 이유는 게임 관련 상태를 GameBoard 컴포넌트에 위치 시키기 위함일까요? 혹시 그렇다면 utils 대신 custom hook으로 Game 관련된 hook을 분리해서 state를 같이 관리하고 그 hook을 그대로 import해서 사용하는 것은 어떨까요?

이번에 제 과제를 할 때 시간 투자를 많이 못해서 함수 분리(utils나 custom hook 등)를 거의 못해서.. 이런 리뷰를 드리기 조금 죄송스럽지만 이에 대해서 어떤 의견을 가지고 계신지 궁금합니다!


const handleClick = (number, index) => {
handleNumberClick({
number,
currentNumber,
initialLength,
remainingLength,
handleInitialNumClick: () => handleInitialNumClick(index),
handleUpcomingNumClick: () => handleUpcomingNumClick(index),
setGameComplete,
onFirstClick,
onLastClick,
});
};

const handleCloseModal = () => {
reset();
};

return (
<GameBoardContainer>
<NextNumber currentNumber={currentNumber} />
<GameBox $gridLength={gridLength}>
{initialNums.map((number, index) =>
number !== null ? (
<NumberButton
key={index}
onClick={() => handleClick(number, index)}
disabled={number < currentNumber}
isClicked={clickedIndexes.has(index)}
>
{number}
</NumberButton>
) : (
<EmptySpace key={index} />
)
Comment on lines +84 to +86

Choose a reason for hiding this comment

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

p1: 여기서는 key가 필요 없을 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

p1: 여기서는 key가 필요 없을 것 같습니다!

맞네요! 감사합니다

)}
Comment on lines +73 to +87
Copy link
Member

Choose a reason for hiding this comment

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

p3:
숫자가 없는 자리를 EmptySpace로 두어 빈 공간을 정리하는 방식이 너무 좋은 것 같아요!
저는 빈 공간을 따로 정의하지 않고 isNew라는 상태를 받아서 opacity로 셀을 안 보이게 했거든요.
EmptySpace 덕분에 그리드 레이아웃이 더 깔끔하게 유지되는 것 같아요! 👏 아이디어 얻어갑니다...

</GameBox>
{gameComplete && (
<CompleteModal onClose={handleCloseModal}>
<Message>게임 기록: {currentTime}</Message>
<CloseButton onClick={handleCloseModal}>닫기</CloseButton>
</CompleteModal>
)}
</GameBoardContainer>
Comment on lines +89 to +95

Choose a reason for hiding this comment

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

p3: 여기서
<CloseButton onClick={handleCloseModal}>닫기</CloseButton>를 따로 두신 것 같아서
onClose={handleCloseModal}가 필요 없을 것 같은데 혹시 다른 역할이 있을지 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

p3: 여기서 <CloseButton onClick={handleCloseModal}>닫기</CloseButton>를 따로 두신 것 같아서 onClose={handleCloseModal}가 필요 없을 것 같은데 혹시 다른 역할이 있을지 궁금합니다!

처음에 내부 모달 내부 요소들을 chidren으로 넘겨서 사용하기전에 작성하였던 부분인데 지우지 못했네요. 꼼꼼한 코드리뷰 감사합니다!

);
};

export default GameBoard;

const GameBoardContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;

const GameBox = styled.div`
display: grid;
grid-template-columns: repeat(${(props) => props.$gridLength}, 1fr);
gap: 1rem;
padding: 2rem;
`;

const blink = keyframes`
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
`;

const NumberButton = styled.button.withConfig({
shouldForwardProp: (prop) => prop !== "isClicked",
})`
Comment on lines +120 to +122
Copy link
Member

Choose a reason for hiding this comment

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

p3:
오,,, shouldForwardProp을 걸 이 코드를 통해 처음 알게 됐어요!
isClicked prop이 DOM에 전달되지 않도록 해주신 거 맞나요?
이 기능은 처음 봤는데 DOM에 불필요한 props가 남지 않도록 관리할 수 있어서 좋은 것 같아요 ㅎㅎ
저도 props를 사용한 스타일링에서 DOM에 전달되지 않도록 shouldForwardProp을 써봐야겠어요!

Comment on lines +120 to +122

Choose a reason for hiding this comment

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

p3: 이렇게 작성해도 styled-components 스타일링을 위해 props는 전달되지만 실제 DOM 요소에는 전달되지 않는군요!!!

단순히 저는 $키워드를 써서 이 작업을 해줬는데, shouldForwardProp는 어떤 prop을 DOM에 전달할지 또는 전달하지 않을지 명시적으로 제어할 수 있다는 점이 다른 것 같네요! $로 할 수 없는 조건을 추가하거나 복잡한 로직을 사용할 때 더 좋을 것 같아요. 좋은 지식 배워갑니다👍

width: 10rem;
height: 10rem;
font-size: 3rem;
font-weight: 700;
border-radius: 10px;
background-color: ${({ isClicked, theme }) =>
isClicked ? theme.colors.thumnail : theme.colors.orange1};
transition: background-color 0.5s;
${({ isClicked }) =>
isClicked &&
css`
animation: ${blink} 0.3s;
`}
`;

const EmptySpace = styled.div`
width: 10rem;
height: 10rem;
background-color: transparent;
visibility: hidden;
`;

const Message = styled.div`
font-size: 3rem;
font-weight: 700;
color: ${({ theme }) => theme.colors.black1};
`;

const CloseButton = styled.button`
display: flex;
justify-content: center;
align-items: center;
width: 10rem;
height: 5rem;
background-color: ${({ theme }) => theme.colors.black1};
color: ${({ theme }) => theme.colors.white1};
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;

&:hover {
background-color: ${({ theme }) => theme.colors.gray1};
}
`;
13 changes: 13 additions & 0 deletions week3/number-game/src/components/Home/GameBoard/NextNumber.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from "styled-components";

const NextNumber = ({ currentNumber }) => {
return <NextNumberContainer>다음 숫자: {currentNumber}</NextNumberContainer>;
};

export default NextNumber;

const NextNumberContainer = styled.div`
font-size: 3.5rem;
font-weight: 900;
color: ${({ theme }) => theme.colors.black1};
`;
Loading