-
Notifications
You must be signed in to change notification settings - Fork 1
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주차 기본/심화/공유 과제 ] 재미있는 카드게임 #3
base: main
Are you sure you want to change the base?
Changes from 16 commits
bef4200
fd4729b
dcb74fa
fea7112
777366e
b0a87ef
6d34f54
6d62890
9b156d2
1cf13ad
272a938
df6990d
a43ae65
a27f074
7c60fe3
71827fc
7cc71c7
5d55c4b
e0b06c3
a23b2ad
4e6dc1c
94b08a4
d8cc8a4
63d24cc
4ede787
a1329f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
module.exports = { | ||
plugins: ['react', 'prettier'], | ||
extends: [ | ||
'react-app', | ||
'plugin:prettier/recommended', | ||
'airbnb', | ||
'plugin:import/errors', | ||
'plugin:import/warnings', | ||
'plugin:prettier/recommended', | ||
], | ||
rules: { | ||
'linebreak-style': 0, | ||
'prettier/prettier': 0, | ||
'import/extensions': 0, | ||
'import/no-unresolved': 0, | ||
'import/no-extraneous-dependencies': 0, // 테스트 또는 개발환경을 구성하는 파일에서는 devDependency 사용을 허용 | ||
'import/prefer-default-export': 0, | ||
'react/prop-types': 0, | ||
'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], | ||
'jsx-a11y/no-noninteractive-element-interactions': 0, | ||
'eol-last': ['error', 'always'], // line의 가장 마지막 줄에는 개행 넣기 | ||
'simple-import-sort/imports': 'error', // import 정렬 | ||
'no-multi-spaces': 'error', | ||
}, | ||
}; |
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? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"semi": true, | ||
"tabWidth": 4, | ||
"printWidth": 120, | ||
"trailingComma": "all", | ||
"bracketSameLine": true, | ||
"singleQuote": true, | ||
"endOfLine": "auto" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
{ | ||
"extends": ["stylelint-config-standard", "stylelint-config-styled-components"], | ||
"plugins": ["stylelint-order"], | ||
"customSyntax": "postcss-styled-syntax", | ||
"overrides": [ | ||
{ | ||
"customSyntax": "@stylelint/postcss-css-in-js", | ||
"files": ["**/*.ts"] | ||
} | ||
], | ||
"rules": { | ||
"declaration-empty-line-before": [ | ||
"always", | ||
{ | ||
"ignore": ["first-nested", "after-comment", "after-declaration", "inside-single-line-block"] | ||
} | ||
], | ||
"order/order": ["custom-properties", "declarations"], | ||
|
||
"order/properties-order": [ | ||
{ | ||
"groupName": "Layout", | ||
"emptyLineBefore": "always", | ||
"noEmptyLineBetween": true, | ||
"properties": [ | ||
"display", | ||
"justify-content", | ||
"align-items", | ||
"flex-direction", | ||
"flex-wrap", | ||
"flex-flow", | ||
"position", | ||
"top", | ||
"right", | ||
"bottom", | ||
"left", | ||
"float", | ||
"clear", | ||
"visibility", | ||
"overflow", | ||
"z-index" | ||
] | ||
}, | ||
{ | ||
"groupName": "Box", | ||
"emptyLineBefore": "always", | ||
"noEmptyLineBetween": true, | ||
"properties": [ | ||
"width", | ||
"height", | ||
"margin", | ||
"padding", | ||
"padding-top", | ||
"padding-right", | ||
"padding-bottom", | ||
"padding-left", | ||
"margin-top", | ||
"margin-right", | ||
"margin-bottom", | ||
"margin-left" | ||
] | ||
}, | ||
{ | ||
"groupName": "Background", | ||
"emptyLineBefore": "always", | ||
"noEmptyLineBetween": true, | ||
"properties": [ | ||
"border", | ||
"background", | ||
"background-color", | ||
"color", | ||
"font-style", | ||
"font-weight", | ||
"font-size", | ||
"line-height", | ||
"letter-spacing", | ||
"text-align", | ||
"text-indent", | ||
"vertical-align" | ||
] | ||
}, | ||
{ | ||
"groupName": "Text", | ||
"emptyLineBefore": "always", | ||
"noEmptyLineBetween": true, | ||
"properties": ["text-decoration", "text-align", "vertical-align"] | ||
}, | ||
{ | ||
"groupName": "ETC", | ||
"emptyLineBefore": "always", | ||
"noEmptyLineBetween": true, | ||
"properties": ["white-space"] | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!DOCTYPE html> | ||
<html lang="ko"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/png" href="/logo.png" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Sugar Rush Player Match🍭</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.jsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "cardgame", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "vite build", | ||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"@emotion/is-prop-valid": "^1.2.2", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-router-dom": "^6.23.0", | ||
"styled-components": "^6.1.8" | ||
}, | ||
"devDependencies": { | ||
"@types/react": "^18.2.66", | ||
"@types/react-dom": "^18.2.22", | ||
"@vitejs/plugin-react-swc": "^3.5.0", | ||
"eslint": "^8.57.0", | ||
"eslint-plugin-react": "^7.34.1", | ||
"eslint-plugin-react-hooks": "^4.6.0", | ||
"eslint-plugin-react-refresh": "^0.4.6", | ||
"vite": "^5.2.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Router from './Router'; | ||
import { ThemeProvider } from 'styled-components'; | ||
import { theme } from './style/theme'; | ||
import { GlobalStyle } from './style/globalStyle'; | ||
|
||
export default function App() { | ||
return ( | ||
<> | ||
<ThemeProvider theme={theme}> | ||
<GlobalStyle /> | ||
<Router /> | ||
</ThemeProvider> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from 'react'; | ||
import { BrowserRouter, Route, Routes } from 'react-router-dom'; | ||
import GamePage from './pages/GamePage'; | ||
import styled from 'styled-components'; | ||
|
||
export default function Router() { | ||
return ( | ||
<> | ||
<BrowserRouter> | ||
<GlobalWrapper> | ||
<Routes> | ||
<Route path="/" element={<GamePage />} /> | ||
</Routes> | ||
</GlobalWrapper> | ||
</BrowserRouter> | ||
</> | ||
); | ||
} | ||
|
||
const GlobalWrapper = styled.main` | ||
margin: 0 auto; | ||
`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 요소가 Routes 상위에 있는 걸로 보아 전역 스타일 역할을 하는 것 같은데 GlobalStyle로 빼주는 건 어떨까요~? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아! 이해했습니다! 수정하겠습니다~~ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React, { useState } from 'react'; | ||
import * as C from './CardStyle'; | ||
|
||
export default function Card(props) { | ||
return ( | ||
<C.CardWrapper $isOpen={props.open}> | ||
<C.FrontCard $img={props.img}></C.FrontCard> | ||
<C.BackCard></C.BackCard> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구조분해할당으로 받아서 가독성을 높여봅시다 ㅎㅎ! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵!! 충고 감사합니다! 수정했습니다! |
||
</C.CardWrapper> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import card1 from './../../assets/images/adora.png'; | ||
import card2 from './../../assets/images/candlehead.png'; | ||
import card3 from './../../assets/images/citrusella.png'; | ||
import card4 from './../../assets/images/crumbelina.png'; | ||
import card5 from './../../assets/images/vanellope.png'; | ||
import card6 from './../../assets/images/jubileena.png'; | ||
import card7 from './../../assets/images/rancis.png'; | ||
import card8 from './../../assets/images/snowanna.png'; | ||
import card9 from './../../assets/images/taffyta.png'; | ||
|
||
const CARDLIST = [ | ||
{ id: 1, img: card1 }, | ||
{ id: 2, img: card2 }, | ||
{ id: 3, img: card3 }, | ||
{ id: 4, img: card4 }, | ||
{ id: 5, img: card5 }, | ||
{ id: 6, img: card6 }, | ||
{ id: 7, img: card7 }, | ||
{ id: 8, img: card8 }, | ||
{ id: 9, img: card9 }, | ||
]; | ||
export default CARDLIST; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,94 @@ | ||||||
import React, { useEffect, useState } from 'react'; | ||||||
import Card from './Card'; | ||||||
import CARDLIST from './CardData'; | ||||||
import styled from 'styled-components'; | ||||||
|
||||||
export default function CardHandler({ level, updateScore, shuffle }) { | ||||||
const [cards, setCards] = useState([]); | ||||||
const [selectedCard, setSelectedCard] = useState(null); // 전에 선택한 카드 | ||||||
const [matchCards, setMatchCards] = useState([]); // 매칭된 카드들 | ||||||
const [openCards, setOpenCards] = useState([]); // 오픈된 카드들 (선택한 카드 + 매칭된 카드들) | ||||||
|
||||||
// 랜덤 카드 뽑기 | ||||||
const getRandomElement = (arr) => { | ||||||
console.log('getRandomElement'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. console문 지워주기!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이걸 놓쳤네요......ㅎ 감사합니다 |
||||||
let result = new Set(); | ||||||
while (result.size < level) { | ||||||
const randomElement = arr[Math.floor(Math.random() * arr.length)]; | ||||||
result.add(randomElement); | ||||||
} | ||||||
return Array.from(result); | ||||||
Comment on lines
+15
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복제거로 Set 사용한 거 신박하네요!!!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구글링하다가 좋아 보여서 하게 되었습니당....ㅎ |
||||||
}; | ||||||
|
||||||
// 카드 쌍 만들기 | ||||||
const initialCards = () => { | ||||||
const selectedCards = getRandomElement(CARDLIST); | ||||||
const cardPairs = selectedCards.reduce( | ||||||
(acc, card) => [ | ||||||
...acc, | ||||||
{ ...card, id: card.id + 1000 }, // ID 충돌 방지 | ||||||
{ ...card, id: card.id + 2000 }, // ID 충돌 방지 | ||||||
], | ||||||
[], | ||||||
); | ||||||
return cardPairs.sort(() => 0.5 - Math.random()); | ||||||
}; | ||||||
|
||||||
useEffect(() => { | ||||||
setCards(initialCards()); | ||||||
setSelectedCard(null); | ||||||
setMatchCards([]); | ||||||
setOpenCards([]); | ||||||
}, [level, shuffle]); | ||||||
Comment on lines
+37
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 연관된게 많아 확실하진 않지만 level을 변경해주는 함수인 updateLevel함수와 reset하는 함수 resetGame에 필요한 로직인 것 같네요 그러면 resetGame에만 useEffect내부 로직을 적용해주고 resetGame함수를 이곳에 호출해볼까요?
|
||||||
|
||||||
function chooseCard(e, id) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이부분도 화살표함수로 사용하면 더 좋을 것 같은데 사용하신 다른 이유가 있으신가요? |
||||||
e.stopPropagation(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 이렇게 이벤트 전파를 방지시킨 특별한 이유가 있을까요?? 만약 이유가 있다고 하더라고, e.stopPropagation()으로 강제로 이벤트 전파를 막는 방식은 지양해야 합니다!! 무엇이든지 앞에 "강제"가 붙는 행위는 사이드 이펙트가 발생할 가능성을 내포하고 있기 때문이에요!! 만약 나중에 이벤트 전파를 막아야 하는 상황이 온다면, 그것은 코드 구조를 잘못 짰다는 방증이니 최대한 코드 구조를 수정해서 이벤트 전파를 막는 방향으로 수정하는 것이 맞습니다!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉....저를 정확히 알고 계십니다..... |
||||||
// 매칭된 카드와 자기 자신은 선택 불가 | ||||||
if (!matchCards.includes(id) && id !== selectedCard) { | ||||||
setOpenCards((prev) => [...prev, id]); // 카드 오픈 | ||||||
if (selectedCard == null) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null은 falsy한 값이니 이렇게 써줄수도 있겠네요!!
Suggested change
물론 selectedCard가 null인 경우와 다른 falsy한 값(0 등)일 경우가 다른 경우로 분기되어야 하는 상황이라면 채현이가 작성한 대로 하는 것이 맞습니다!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 쓰니 훨씬 코드가 간결해지네요! 감사합니다 수정했습니다👍🏻👍🏻 |
||||||
// 선택한 카드가 없을 때 | ||||||
setSelectedCard(id); // 선택한 카드에 추가 | ||||||
} else if (selectedCard % 1000 !== id % 1000) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1000이라는 숫자가 검증을 하기 위한 값으로 자주 등장하는 것 같은데, 이런 값은 이름을 부여해 불변값으로 별도로 관리해줘도 좋을 것 같다는 생각이 드네요 :) 코드를 처음 보는 사람이 이 로직을 보고도 무슨 의미인지 알 수 있도록 하기 위해서!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저두 1000을 쓰면서 찝찌입하긴 했는데 역시 코리가 달리네요..헤헤...... |
||||||
// 카드가 매칭되지 않을 때 | ||||||
setTimeout(() => { | ||||||
// 1초 지연 | ||||||
setOpenCards(matchCards); // 선택 카드 다시 닫기 | ||||||
setSelectedCard(null); // 선택한 카드 비우기 | ||||||
}, 500); | ||||||
} else { | ||||||
//카드가 매칭되었을 때 | ||||||
setMatchCards((prev) => { | ||||||
const newMatchCards = [...prev, id, selectedCard]; | ||||||
setOpenCards(newMatchCards); // 매칭 반영 후 오픈 고정 | ||||||
return newMatchCards; | ||||||
}); | ||||||
setSelectedCard(null); //선택한 카드 비우기 | ||||||
updateScore((currentScore) => currentScore + 1); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
return ( | ||||||
<CardContainer> | ||||||
{cards.map((card, index) => ( | ||||||
<CardWrapper key={index} onClick={(e) => chooseCard(e, card.id)}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. key 값으로 map의 순회 index를 주는 것은 지양해야 합니다!! 고유하게 사용할 마땅한 값이 없으면 다음과 같이 해주는 방법도 많이 사용합니다!!
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 이런 방법이 있다니 신세계입니다! 적용 완료했습니다~~ㅎㅎ |
||||||
<Card img={card.img} open={openCards.includes(card.id)} /> | ||||||
</CardWrapper> | ||||||
))} | ||||||
</CardContainer> | ||||||
); | ||||||
} | ||||||
|
||||||
const CardContainer = styled.section` | ||||||
display: flex; | ||||||
flex-wrap: wrap; | ||||||
align-items: center; | ||||||
justify-content: center; | ||||||
`; | ||||||
|
||||||
const CardWrapper = styled.span` | ||||||
display: inline-block; | ||||||
margin: 1rem; | ||||||
cursor: pointer; | ||||||
`; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 전체적으로 Style 파일을 별도로 관리하는 전략을 사용한 것 같은데, 통일성을 위해 이 부분도 별도의 Style 파일을 만들어 관리하는 것이 좋을 것 같아요!! 나중에 스타일을 수정해야 할 때 별도의 Style 파일에서 수정해야 할지, 렌더 컴포넌트가 있는 파일에서 수정해야 할지 헤맬 수 있습니다!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나로 통일! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리액트 라우터가 업데이트 되면서,
공식문서에서 createBrowserRouter 사용을 더 권하는 것 같아요!
링크 남겨둘테니 저희 합세에선 이렇게 적용해봅시다 ㅎㅎ!
https://reactrouter.com/en/main/start/overview
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
옹 하나 배워갑니다!!! @ExceptAnyone
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우앙 처음 알았네요....!! 배워갑니다 ✨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 처음 보는건데 신기하네요! 합세때 적용할 수 있도록 수정했습니다