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주차 기본/심화/생각 과제] 춘식이 카드 뒤집기 게임 #7

Open
wants to merge 16 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/cardMatchingGame/.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?
14 changes: 14 additions & 0 deletions week3/cardMatchingGame/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>카드 맞추기 게임</title>
</head>
<body>
<div id="root"></div>
<div id="modal-root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions week3/cardMatchingGame/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "cardmatchinggame",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.10"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"vite": "^4.3.2"
}
}
1 change: 1 addition & 0 deletions week3/cardMatchingGame/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.
61 changes: 61 additions & 0 deletions week3/cardMatchingGame/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useEffect } from "react";
import styled, { ThemeProvider } from "styled-components";
import GlobalStyle from "./styles/reset";
import Cards from "./components/Cards";
import theme from "./styles/theme";
import Header from "./components/Header";
import { useState } from "react";
import Modal from "./components/Modal";

export default function App() {
const [score, setScore] = useState(0);
const [level, setLevel] = useState("EASY");
const [resetClicked, setResetClicked] = useState(false);
const [showModal, setShowModal] = useState(false);
Comment on lines +11 to +14
Copy link
Member

Choose a reason for hiding this comment

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

아니 상태관리 진짜 깔끔하다. 나는 현수랑 다르게 카드와 관련된 상태를 모두 최상단에서 만들어서 내려줬거든. 그렇게 했던 이유가 level이 바뀔 때마다 카드도 바꿔줘야하니까 그랬던건데 밑에 보니까 현수는 useState로 level이 바뀌면 카드 리스트를 새로 만들어내도록 했구나... useState를 어떻게 써야하는지 잘 몰라서 복잡하게 구현했던 것 같아 ㅋㅋㅋ 이렇게 깔끔하게 할 수 있군!!


const levels = {
EASY: 5,
NORMAL: 7,
HARD: 9,
};
const changeLevel = (level) => {
setLevel(level);
};
const changeScore = (score) => {
setScore(score);
};
const changeResetClicked = () => {
setResetClicked(!resetClicked);
};

const handleClose = () => {
setShowModal(false);
};

useEffect(() => {
if(score === levels[level]){
setShowModal(true)
}
}, [score]);

return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Header
changeLevel={changeLevel}
levels={levels}
level={level}
score={score}
changeResetClicked={changeResetClicked}
/>
<Cards
levels={levels}
level={level}
score={score}
changeScore={changeScore}
resetClicked={resetClicked}
/>
<Modal isOpen={showModal} onClose={handleClose} />
Copy link

Choose a reason for hiding this comment

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

전체적으로 하위 -> 상위로 전달되는 핸들러는 최상단 컴포넌트에서 지정해주고,
많은 props drilling이 되는 부분은 딱 공통된 상단에 묶어놓은 게 인상깊다 !

pr point를 읽으면서도 구현계획이 명확해서 신기했는데,
혹시 이런 props를 설계할 때 이건 어떤 부분에서 많이 사용될거니까 여기에 지정하는 게 낫겠다라고 생각하는
주요 포인트가 있을까??

적재적소에 잘 불러서 깔끔하게 짠 것 같아서 넘 잘 읽었어 🥹🔥

Copy link
Member

Choose a reason for hiding this comment

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

최상단에서 필요한 state와 변수, 함수들을 딱 정리해서 필요한 컴포넌트에 내려줬다는게 한눈에 보인다! 너무 깔끔해..!!!!

</ThemeProvider>
);
}
18 changes: 18 additions & 0 deletions week3/cardMatchingGame/src/ModalProtal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import ReactDOM from "react-dom";

const ModalPortal = ({ children }) => {
const modalRoot = document.getElementById("modal-root");
const el = document.createElement("div");

React.useEffect(() => {
modalRoot.appendChild(el);
return () => {
modalRoot.removeChild(el);
};
}, [modalRoot, el]);

return ReactDOM.createPortal(children, el);
};

export default ModalPortal;
Binary file added week3/cardMatchingGame/src/assets/backCardImg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions week3/cardMatchingGame/src/assets/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export const data = [
{
id: 0,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452854.gif",
},
{
id: 1,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452854.gif",
},
{
id: 2,
cardImg: "https://tenor.com/ko/view/chunsik-춘식-춘식이-gif-24725088.gif",
},
{
id: 3,
cardImg: "https://tenor.com/ko/view/chunsik-춘식-춘식이-gif-24725088.gif",
},
{
id: 4,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24725094.gif",
},
{
id: 5,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24725094.gif",
},
{
id: 6,
cardImg: "https://tenor.com/ko/view/chunsik-춘식-춘식이-gif-24452956.gif",
},
{
id: 7,
cardImg: "https://tenor.com/ko/view/chunsik-춘식-춘식이-gif-24452956.gif",
},
{
id: 8,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452902.gif",
},
{
id: 9,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452902.gif",
},
{
id: 10,
cardImg: "https://tenor.com/ko/view/춘식이-춘식-chunsik-gif-24691909.gif",
},
{
id: 11,
cardImg: "https://tenor.com/ko/view/춘식이-춘식-chunsik-gif-24691909.gif",
},
{
id: 12,
cardImg: "https://tenor.com/ko/view/춘식이-춘식-chunsik-gif-24452916.gif",
},
{
id: 13,
cardImg: "https://tenor.com/ko/view/춘식이-춘식-chunsik-gif-24452916.gif",
},
{
id: 14,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452897.gif",
},
{
id: 15,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24452897.gif",
},
{
id: 16,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24691897.gif",
},
{
id: 17,
cardImg: "https://tenor.com/ko/view/춘식-춘식이-chunsik-gif-24691897.gif",
},
]
125 changes: 125 additions & 0 deletions week3/cardMatchingGame/src/components/Cards.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { useEffect, useState, useMemo } from "react";
import { data } from "../assets/data";
import styled from "styled-components";
import theme from "../styles/theme";
import backCardImg from "../assets/backCardImg.jpg";

export default function Cards(props) {
const { levels, level, score, changeScore, resetClicked } = props;
const [cards, setCards] = useState([]);
const [clickedCards, setClickedCards] = useState([]);
const [matchedCards, setMatchedCards] = useState([]);

//쌍을 유지하면서 순서 섞기
function shufflePairs(arr) {
const pairs = [];
for (let i = 0; i < arr.length; i += 2) {
pairs.push([arr[i], arr[i + 1]]);
}
for (let i = pairs.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[pairs[i], pairs[j]] = [pairs[j], pairs[i]];
}
const shuffled = pairs.flatMap((pair) => pair);
Copy link

Choose a reason for hiding this comment

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

flat flatmap 이라는 평탄화용 메소드가 있구나 !!
배열의 메소드 세계란.. 너무 유용한 것 같은데 ~~

return shuffled;
}

//카드 클릭 시, clickedCards 배열에 카드 id 추가
const handleCardClick = (cardId) => {
//clickedCards 최대 길이 2로 제한
if (clickedCards.length < 2) {
setClickedCards((prev) => [...prev, cardId]);
}
};

//레벨에 따라 카드 랜덤하게 선택하고 랜덤하게 배열
useEffect(() => {
setCards(
shufflePairs(data)
.slice(0, levels[level] * 2)
.sort(() => Math.random() - 0.5)
);
}, [level]);

//클릭된 카드가 서로 같은 카드인지 체크하고, 같은 카드이면 matchedCards 배열에 저장
useEffect(() => {
//클릭된 카드가 2개 미만이거나 같은 카드를 연속 클릭 시 조기 리턴
if (clickedCards.length < 2) {
return;
}
Comment on lines +46 to +49
Copy link

Choose a reason for hiding this comment

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

조기리턴으로 2개 미만의 카드 선택시의 상황을 아주 깔끔하게 만들어주었구나..
비슷하게 매치된 애들 / 클릭되어 후보인 애들로 나눠서 상태를 관리한 것 같은데 전체적인 코드 가독성은
좀 더 좋은 것 같아서 많이 참고해가 ㅎㅎ

똒똒해 ... ### 👍


const [card1, card2] = clickedCards;
if (card1 === card2) {
clickedCards.splice(1, 1);
return;
}
Comment on lines +52 to +55
Copy link
Member

Choose a reason for hiding this comment

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

연속클릭되는 상황은 생각을 못했는데 꼼꼼한 예외처리 진짜 멋지다!! 그냥 냅두고 넘어갈수도 있었을텐데!!! 👍 나도 리팩토링해보께


if (data[card1].cardImg === data[card2].cardImg) {
Copy link
Member

Choose a reason for hiding this comment

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

모든 card 각각을 구분하는 것과 card img의 일치 여부를 분리하는게 되게 복잡했는데, 이런식으로 한건 또 처음본다!! 나는 카드가 섞이면서 계속 번호가 바뀌니까 이렇게 할 생각은 못했는데.... 굉장히 합리적인 방법인 것 같아 오래 고민한게 느껴지는 깔끔한 로직...👍

if (!matchedCards.includes(card1) && card1 !== card2) {
changeScore(score + 1);
}
setMatchedCards((prev) => [...prev, card1, card2]);
setTimeout(() => setClickedCards([]), 1000);
} else {
setTimeout(() => setClickedCards([]), 1000);
}
}, [clickedCards]);

//게임 도중에 레벨을 바꾸면 진행 상황 초기화
useEffect(() => {
changeScore(0);
setClickedCards([]);
setMatchedCards([]);
}, [level, resetClicked]);

return (
<CardsContainer>
{cards.map((item) => (
<Card
key={item.id}
id={item.id}
cardImg={item.cardImg}
onClick={() => handleCardClick(item.id)}
matchedCards={matchedCards.includes(item.id) ? true : false}
clickedCards={clickedCards.includes(item.id) ? true : false}
/>
))}
</CardsContainer>
);
}

const CardsContainer = styled.div`
display: flex;
flex-wrap: wrap;
background-color: ${theme.colors.lightYellow};
& > div {
& > img {
width: 25rem;
height: 25rem;
}
}
`;

const Card = styled.div`
background-image: url(${(props) =>
props.matchedCards
? props.cardImg
: props.clickedCards
? props.cardImg
: backCardImg});
Comment on lines +104 to +109
Copy link
Member

Choose a reason for hiding this comment

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

style에서 props나 비교연산자를 쓸 생각은 못했는데 똑똑한 방법 배워간다..!!

width: 20rem;
height: 20rem;
background-size: cover;
background-position: center;
cursor: pointer;
margin: 6rem;
transform-style: preserve-3d;
transition: transform 0.6s ease-in-out;
transform: ${(props) =>
props.matchedCards
? "rotateY(360deg)"
: props.clickedCards
? "rotateY(360deg)" // card1
: "rotateY(0deg)"}; // card2
backface-visibility: ${(props) => props.cardImg};
`;
Loading