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주차 기본/심화/공유 과제 ] 재미있는 카드게임 #3

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bef4200
init: 초기 세팅
imddoy Apr 29, 2024
fd4729b
feat: 카드 랜덤 배치 및 클릭시 뒤집기
imddoy Apr 29, 2024
dcb74fa
feat: 카드 뒤집기 구현
imddoy Apr 30, 2024
fea7112
design: 카드 디자인 수정
imddoy Apr 30, 2024
777366e
feat: 스코어 구현
imddoy Apr 30, 2024
b0a87ef
feat: 카드 게임 난이도 설정
imddoy Apr 30, 2024
6d34f54
docs: 리드미 수정
imddoy Apr 30, 2024
6d62890
feat: reset 구현
imddoy Apr 30, 2024
9b156d2
rename: 폴더명 변경
imddoy Apr 30, 2024
1cf13ad
feat: 모달 오픈
imddoy Apr 30, 2024
272a938
feat: 모달 클로즈
imddoy Apr 30, 2024
df6990d
chore: 컴포넌트명 변경
imddoy Apr 30, 2024
a43ae65
remove: 콘솔로그 삭제
imddoy Apr 30, 2024
a27f074
design: reset z-index
imddoy May 1, 2024
7c60fe3
design: 파비콘
imddoy May 1, 2024
71827fc
design: 선택된 레벨 표시
imddoy May 1, 2024
7cc71c7
chore: 상수데이터 분리
imddoy May 8, 2024
5d55c4b
refactor: 구조분해할당 이용
imddoy May 8, 2024
e0b06c3
refactor: css 속성 순서 정렬 및 rem
imddoy May 8, 2024
a23b2ad
chore: null 사용 대신 ! 사용
imddoy May 8, 2024
4e6dc1c
chore: style 분리
imddoy May 8, 2024
94b08a4
chore: id 부여 숫자 상수로 관리
imddoy May 8, 2024
d8cc8a4
chore: 콘솔로그 삭제
imddoy May 8, 2024
63d24cc
chore: 불필요한 globalwrapper 삭제
imddoy May 8, 2024
4ede787
chore: 레벨 초기화때 숫자가 아닌 상수 사용하기
imddoy May 8, 2024
a1329f9
refactor: createBrowserRouter 사용
imddoy May 8, 2024
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
25 changes: 25 additions & 0 deletions week3_cardgame/cardgame/.eslintrc.cjs
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',
},
};
24 changes: 24 additions & 0 deletions week3_cardgame/cardgame/.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?
9 changes: 9 additions & 0 deletions week3_cardgame/cardgame/.prettierrc
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"
}
96 changes: 96 additions & 0 deletions week3_cardgame/cardgame/.stylelintrc
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"]
}
]
}
}
13 changes: 13 additions & 0 deletions week3_cardgame/cardgame/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" />
<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>
29 changes: 29 additions & 0 deletions week3_cardgame/cardgame/package.json
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"
}
}
Binary file added week3_cardgame/cardgame/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions week3_cardgame/cardgame/src/App.jsx
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>
</>
);
}
22 changes: 22 additions & 0 deletions week3_cardgame/cardgame/src/Router.jsx
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>
</>
Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

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

옹 하나 배워갑니다!!! @ExceptAnyone

Choose a reason for hiding this comment

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

우앙 처음 알았네요....!! 배워갑니다 ✨

Copy link
Contributor Author

@imddoy imddoy May 7, 2024

Choose a reason for hiding this comment

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

오 처음 보는건데 신기하네요! 합세때 적용할 수 있도록 수정했습니다

);
}

const GlobalWrapper = styled.main`
margin: 0 auto;
`;
Copy link
Member

Choose a reason for hiding this comment

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

이 요소가 Routes 상위에 있는 걸로 보아 전역 스타일 역할을 하는 것 같은데 GlobalStyle로 빼주는 건 어떨까요~?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아! 이해했습니다! 수정하겠습니다~~

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions week3_cardgame/cardgame/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.
11 changes: 11 additions & 0 deletions week3_cardgame/cardgame/src/components/card/Card.jsx
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>
Copy link
Member

Choose a reason for hiding this comment

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

구조분해할당으로 받아서 가독성을 높여봅시다 ㅎㅎ!

Copy link
Contributor Author

@imddoy imddoy May 7, 2024

Choose a reason for hiding this comment

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

넵!! 충고 감사합니다! 수정했습니다!

</C.CardWrapper>
);
}
22 changes: 22 additions & 0 deletions week3_cardgame/cardgame/src/components/card/CardData.jsx
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;
94 changes: 94 additions & 0 deletions week3_cardgame/cardgame/src/components/card/CardHandler.jsx
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');
Copy link
Member

Choose a reason for hiding this comment

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

console문 지워주기!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

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

중복제거로 Set 사용한 거 신박하네요!!!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 연관된게 많아 확실하진 않지만 level을 변경해주는 함수인 updateLevel함수와 reset하는 함수 resetGame에 필요한 로직인 것 같네요 그러면 resetGame에만 useEffect내부 로직을 적용해주고 resetGame함수를 이곳에 호출해볼까요?

const updateLevel = (level) => {
          resetGame()      
        setLevel(level);
    };


function chooseCard(e, id) {
Copy link
Contributor

Choose a reason for hiding this comment

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

이부분도 화살표함수로 사용하면 더 좋을 것 같은데 사용하신 다른 이유가 있으신가요?

e.stopPropagation();
Copy link
Member

Choose a reason for hiding this comment

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

혹시 이렇게 이벤트 전파를 방지시킨 특별한 이유가 있을까요??
코드 구조를 보면 CardHandler 상위 요소에 특별히 click 이벤트 핸들러가 바인딩되어있지 않은 것 같아서 이 부분이 없어도 클릭 이벤트가 의도대로 잘 동작할 것 같다는 생각이 듭니다!!

만약 이유가 있다고 하더라고, e.stopPropagation()으로 강제로 이벤트 전파를 막는 방식은 지양해야 합니다!! 무엇이든지 앞에 "강제"가 붙는 행위는 사이드 이펙트가 발생할 가능성을 내포하고 있기 때문이에요!!

만약 나중에 이벤트 전파를 막아야 하는 상황이 온다면, 그것은 코드 구조를 잘못 짰다는 방증이니 최대한 코드 구조를 수정해서 이벤트 전파를 막는 방향으로 수정하는 것이 맞습니다!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {
Copy link
Member

Choose a reason for hiding this comment

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

null은 falsy한 값이니 이렇게 써줄수도 있겠네요!!

Suggested change
if (selectedCard == null) {
if (!selectedCard) {

물론 selectedCard가 null인 경우와 다른 falsy한 값(0 등)일 경우가 다른 경우로 분기되어야 하는 상황이라면 채현이가 작성한 대로 하는 것이 맞습니다!!

Copy link
Contributor Author

@imddoy imddoy May 7, 2024

Choose a reason for hiding this comment

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

이렇게 쓰니 훨씬 코드가 간결해지네요! 감사합니다 수정했습니다👍🏻👍🏻

// 선택한 카드가 없을 때
setSelectedCard(id); // 선택한 카드에 추가
} else if (selectedCard % 1000 !== id % 1000) {
Copy link
Member

Choose a reason for hiding this comment

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

1000이라는 숫자가 검증을 하기 위한 값으로 자주 등장하는 것 같은데, 이런 값은 이름을 부여해 불변값으로 별도로 관리해줘도 좋을 것 같다는 생각이 드네요 :) 코드를 처음 보는 사람이 이 로직을 보고도 무슨 의미인지 알 수 있도록 하기 위해서!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)}>
Copy link
Member

Choose a reason for hiding this comment

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

key 값으로 map의 순회 index를 주는 것은 지양해야 합니다!!
idx가 변경되는 이벤트가 발생하면 렌더되는 컴포넌트에서 사용하는 state가 의도하지 않은 방식으로 바뀔 수도 있기 때문이에요!!

고유하게 사용할 마땅한 값이 없으면 다음과 같이 해주는 방법도 많이 사용합니다!!

Suggested change
<CardWrapper key={index} onClick={(e) => chooseCard(e, card.id)}>
<CardWrapper key={`card-${index}`} onClick={(e) => chooseCard(e, card.id)}>

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
`;
Copy link
Member

Choose a reason for hiding this comment

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

지금 전체적으로 Style 파일을 별도로 관리하는 전략을 사용한 것 같은데, 통일성을 위해 이 부분도 별도의 Style 파일을 만들어 관리하는 것이 좋을 것 같아요!! 나중에 스타일을 수정해야 할 때 별도의 Style 파일에서 수정해야 할지, 렌더 컴포넌트가 있는 파일에서 수정해야 할지 헤맬 수 있습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

하나로 통일!
명심하겠습니다!

Loading