Skip to content
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-homework/week3/.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?
12 changes: 12 additions & 0 deletions week3-homework/week3/README.md
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.
33 changes: 33 additions & 0 deletions week3-homework/week3/eslint.config.js
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 },
],
},
},
]
13 changes: 13 additions & 0 deletions week3-homework/week3/index.html
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>
28 changes: 28 additions & 0 deletions week3-homework/week3/package.json
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"
}
}
1 change: 1 addition & 0 deletions week3-homework/week3/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.
41 changes: 41 additions & 0 deletions week3-homework/week3/src/App.jsx
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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

activeTab의 초기값으로 "github" 문자열이 하드코딩되어 있는데 이 값은 이후에 조건부 렌더링에서도 또 사용되고 있습니다. 상수로 분리하여 활용한다면 관리 측면에서 더욱 좋을 것 같습니다


return (
<div css={rootWrapper}>
<Header activeTab={activeTab} onTabChange={setActiveTab} />
<main css={contentWrapper}>
{activeTab === "github" ? (
<GithubSearchContainer />
) : (
<BaseballContainer />
)}
</main>
</div>
);
}

export default App;
1 change: 1 addition & 0 deletions week3-homework/week3/src/assets/ic-close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions week3-homework/week3/src/components/baseball/BaseballContainer.jsx
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 = () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

handleSubmit 함수가 너무 길어 보이며 여러 책임(입력 검증, 게임 로직 처리, 상태 업데이트)을 가지고 있습니다.
이를 validateInput, checkGameResult, updateGameState 등의 더 작은 함수로 분리하면 코드 가독성과 유지보수성이 향상될 것 같습니다!

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;
15 changes: 15 additions & 0 deletions week3-homework/week3/src/components/baseball/HistoryList.jsx
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;
47 changes: 47 additions & 0 deletions week3-homework/week3/src/components/baseball/Input.jsx
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) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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;
7 changes: 7 additions & 0 deletions week3-homework/week3/src/components/baseball/Message.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

function Message({ text }) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Message 컴포넌트가 비록 매우 간단하지만, 별도의 파일로 분리되어 있어 관심사 분리가 잘 구현되어 있다고 생각됩니다. 이 부분은 개인적으로 매우 좋다고 생각됩니다~!

return <p>{text}</p>;
}

export default Message;
Loading