-
Notifications
You must be signed in to change notification settings - Fork 1
[3주차 과제] 숫자 야구 게임 & 깃허브 검색 #5
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
base: main
Are you sure you want to change the base?
Changes from all commits
bafc504
f333b68
dc1cf61
4eeac1e
9298306
5a74075
2e3e983
a573d0b
82380fa
381e794
3a4efd1
690346d
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,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,12 @@ | ||
| # 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) uses [Babel](https://babeljs.io/) for Fast Refresh | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import js from '@eslint/js' | ||
| import globals from 'globals' | ||
| 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', | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, | ||
| 'react-refresh': reactRefresh, | ||
| }, | ||
| rules: { | ||
| ...js.configs.recommended.rules, | ||
| ...reactHooks.configs.recommended.rules, | ||
| 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], | ||
| 'react-refresh/only-export-components': [ | ||
| 'warn', | ||
| { allowConstantExport: true }, | ||
| ], | ||
| }, | ||
| }, | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <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>Vite + React</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,28 @@ | ||
| { | ||
| "name": "week3", | ||
| "private": true, | ||
| "version": "0.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "lint": "eslint .", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { | ||
| "@emotion/react": "^11.14.0", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^9.22.0", | ||
| "@types/react": "^19.0.10", | ||
| "@types/react-dom": "^19.0.4", | ||
| "@vitejs/plugin-react": "^4.3.4", | ||
| "eslint": "^9.22.0", | ||
| "eslint-plugin-react-hooks": "^5.2.0", | ||
| "eslint-plugin-react-refresh": "^0.4.19", | ||
| "globals": "^16.0.0", | ||
| "vite": "^6.3.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { useState } from "react"; | ||
| import { css } from "@emotion/react"; | ||
| import Header from "./components/common/Header"; | ||
| import GithubSearchContainer from "./components/github/GithubSearchContainer"; | ||
| import BaseballContainer from "./components/baseball/BaseballContainer"; | ||
|
|
||
| const rootWrapper = css` | ||
| width: 100vw; | ||
| `; | ||
| const contentWrapper = css` | ||
| max-width: 600px; | ||
| margin: 40px auto 0; | ||
| padding: 24px; | ||
| text-align: center; | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 100%; | ||
| font-family: "Noto Sans KR", sans-serif; | ||
| `; | ||
|
|
||
| function App() { | ||
| const [activeTab, setActiveTab] = useState("github"); | ||
|
|
||
| return ( | ||
| <div css={rootWrapper}> | ||
| <Header activeTab={activeTab} onTabChange={setActiveTab} /> | ||
| <main css={contentWrapper}> | ||
| {activeTab === "github" ? ( | ||
| <GithubSearchContainer /> | ||
| ) : ( | ||
| <BaseballContainer /> | ||
| )} | ||
| </main> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import React, { useState, useEffect } from "react"; | ||
| import { css } from "@emotion/react"; | ||
| import Input from "./Input"; | ||
| import Message from "./Message"; | ||
| import HistoryList from "./HistoryList"; | ||
|
|
||
| function generateRandomNumber() { | ||
| const digits = []; | ||
| while (digits.length < 3) { | ||
| const num = Math.floor(Math.random() * 10); | ||
| if (!digits.includes(num)) digits.push(num); | ||
| } | ||
| return digits; | ||
| } | ||
|
|
||
| // 스타일 정의 | ||
| const containerStyle = css` | ||
| max-width: 480px; | ||
| margin: 40px auto; | ||
| padding: 24px; | ||
| border-radius: 12px; | ||
| `; | ||
|
|
||
| const titleStyle = css` | ||
| text-align: center; | ||
| font-size: 1.5rem; | ||
| margin-bottom: 16px; | ||
| `; | ||
|
|
||
| const attemptStyle = css` | ||
| text-align: center; | ||
| font-size: 1rem; | ||
| margin-bottom: 20px; | ||
| color: #495057; | ||
| `; | ||
|
|
||
| function BaseballContainer() { | ||
| const [answer, setAnswer] = useState([]); | ||
| const [input, setInput] = useState(""); | ||
| const [message, setMessage] = useState(""); | ||
| const [history, setHistory] = useState([]); | ||
| const [attempt, setAttempt] = useState(0); | ||
| const [isGameOver, setIsGameOver] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| setAnswer(generateRandomNumber()); | ||
| }, []); | ||
|
|
||
| const resetGame = () => { | ||
| setAnswer(generateRandomNumber()); | ||
| setInput(""); | ||
| setMessage(""); | ||
| setHistory([]); | ||
| setAttempt(0); | ||
| setIsGameOver(false); | ||
| }; | ||
|
|
||
| const handleSubmit = () => { | ||
|
Member
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. handleSubmit 함수가 너무 길어 보이며 여러 책임(입력 검증, 게임 로직 처리, 상태 업데이트)을 가지고 있습니다. |
||
| if (isGameOver) return; | ||
|
|
||
| if (input.length !== 3 || new Set(input).size !== 3) { | ||
| setMessage("⚠️ 3자리 중복되지 않는 숫자를 입력하세요."); | ||
| return; | ||
| } | ||
|
|
||
| const inputDigits = input.split("").map(Number); | ||
| let strike = 0; | ||
| let ball = 0; | ||
|
|
||
| inputDigits.forEach((num, idx) => { | ||
| if (num === answer[idx]) strike++; | ||
| else if (answer.includes(num)) ball++; | ||
| }); | ||
|
|
||
| let result = ""; | ||
| if (strike === 3) { | ||
| result = "🎉 3S - 승리!"; | ||
| setMessage(result); | ||
| setHistory([{ input, result }, ...history]); | ||
| setIsGameOver(true); | ||
| setTimeout(resetGame, 2000); | ||
| return; | ||
| } | ||
|
|
||
| if (attempt + 1 >= 10) { | ||
| result = "❌ 10회 실패 - 게임 패배!"; | ||
| setIsGameOver(true); | ||
| setTimeout(resetGame, 5000); | ||
| } else if (strike === 0 && ball === 0) { | ||
| result = "OUT"; | ||
| } else { | ||
| result = `${ball}B${strike}S`; | ||
| } | ||
|
|
||
| setHistory([{ input, result }, ...history]); | ||
| setMessage(result); | ||
| setAttempt((prev) => prev + 1); | ||
| setInput(""); | ||
| }; | ||
|
|
||
| return ( | ||
| <div css={containerStyle}> | ||
| <h2 css={titleStyle}>⚾ 숫자 야구 게임</h2> | ||
| <p css={attemptStyle}>시도 횟수: {attempt} / 10</p> | ||
| <Input input={input} setInput={setInput} onSubmit={handleSubmit} /> | ||
| <Message text={message} /> | ||
| <HistoryList history={history} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default BaseballContainer; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import React from "react"; | ||
|
|
||
| function HistoryList({ history }) { | ||
| return ( | ||
| <ul> | ||
| {history.map((item, index) => ( | ||
| <li key={index}> | ||
| 입력: {item.input} → 결과: {item.result} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } | ||
|
|
||
| export default HistoryList; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** @jsxImportSource @emotion/react */ | ||
| import { useState } from "react"; | ||
| import { | ||
| buttonStyle, | ||
| inputStyle, | ||
| inputWrapperStyle, | ||
| } from "../../styles/inputStyles"; | ||
|
|
||
| function Input({ input, setInput, onSubmit }) { | ||
| const [warning, setWarning] = useState(""); | ||
|
|
||
| const handleChange = (e) => { | ||
|
Member
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. 현재 입력 유효성 검사 로직이 구현되어 있는데 BaseballContainer.jsx의 handleSubmit 함수(line: 61-64)에서도 유사한 검증을 수행하고 있어 보입니다. |
||
| const value = e.target.value; | ||
| setInput(value); | ||
|
|
||
| if (value.length > 3) { | ||
| setWarning("⚠️ 3자리까지만 입력 가능합니다."); | ||
| } else { | ||
| setWarning(""); | ||
| } | ||
| }; | ||
|
|
||
| const handleKeyPress = (e) => { | ||
| if (e.key === "Enter") onSubmit(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div> | ||
| <div css={inputWrapperStyle}> | ||
| <input | ||
| type="text" | ||
| value={input} | ||
| css={inputStyle} | ||
| onChange={handleChange} | ||
| onKeyPress={handleKeyPress} | ||
| placeholder="3자리 숫자 입력" | ||
| /> | ||
| <button css={buttonStyle} onClick={onSubmit}> | ||
| 확인 | ||
| </button> | ||
| </div> | ||
| {warning && <p style={{ color: "red" }}>{warning}</p>} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default Input; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import React from "react"; | ||
|
|
||
| function Message({ text }) { | ||
|
Member
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. Message 컴포넌트가 비록 매우 간단하지만, 별도의 파일로 분리되어 있어 관심사 분리가 잘 구현되어 있다고 생각됩니다. 이 부분은 개인적으로 매우 좋다고 생각됩니다~! |
||
| return <p>{text}</p>; | ||
| } | ||
|
|
||
| export default Message; | ||
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.
activeTab의 초기값으로 "github" 문자열이 하드코딩되어 있는데 이 값은 이후에 조건부 렌더링에서도 또 사용되고 있습니다. 상수로 분리하여 활용한다면 관리 측면에서 더욱 좋을 것 같습니다