-
Notifications
You must be signed in to change notification settings - Fork 0
[3주차 기본/심화/공유 과제] 숫자야구 게임 + 깃허브 검색 페이지 #9
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
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,8 @@ | ||
| { | ||
| "singleQuote": true, | ||
| "semi": true, | ||
| "useTabs": false, | ||
| "tabWidth": 2, | ||
| "trailingComma": "all", | ||
| "printWidth": 80 | ||
| } |
| 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,12 @@ | ||
| <!doctype html> | ||
| <html lang="ko"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>숫자야구 + 깃허브</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,32 @@ | ||
| { | ||
| "name": "week3-assignment", | ||
| "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", | ||
| "@emotion/styled": "^11.14.0", | ||
| "emotion-reset": "^3.0.1", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@emotion/babel-plugin": "^11.13.5", | ||
| "@eslint/js": "^9.22.0", | ||
| "@types/react": "^19.0.10", | ||
| "@types/react-dom": "^19.0.4", | ||
| "@vitejs/plugin-react-swc": "^3.8.0", | ||
| "eslint": "^9.22.0", | ||
| "eslint-plugin-react-hooks": "^5.2.0", | ||
| "eslint-plugin-react-refresh": "^0.4.19", | ||
| "globals": "^16.0.0", | ||
| "prettier": "^3.5.3", | ||
| "vite": "^6.3.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { useState } from 'react'; | ||
| import { Global } from '@emotion/react'; | ||
| import globalStyle from './styles/global'; | ||
| import Header from './components/Header/Header'; | ||
| import GithubSearch from './components/GithubSearch/GithubSearch'; | ||
| import NumberBaseballGame from './components/NumberBaseballGame/NumberBaseballGame'; | ||
| import Layout from './components/Layout/Layout'; | ||
|
|
||
| function App() { | ||
| const [tab, setTab] = useState('깃허브'); | ||
|
|
||
| const tabs = { | ||
| 깃허브: <GithubSearch />, | ||
| 야구: <NumberBaseballGame />, | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <Global styles={globalStyle} /> | ||
| <Layout header={<Header selectedTab={tab} onChangeTab={setTab} />}> | ||
| {tabs[tab]} | ||
| </Layout> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { | ||
| cardStyle, | ||
| imageStyle, | ||
| closeButtonStyle, | ||
| nameStyle, | ||
| followInfoWrapper, | ||
| followInfoItem, | ||
| } from './GithubCard.style'; | ||
|
|
||
| const GithubCard = ({ user, onClose }) => { | ||
| if (!user) return 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. 조건부 렌더링을 활용해 user가 없을 때 렌더링을 방지한 점이 좋습니다!👍 |
||
|
|
||
| return ( | ||
| <div css={cardStyle}> | ||
| <button css={closeButtonStyle} onClick={onClose}> | ||
| x | ||
| </button> | ||
| <a | ||
| href={user.html_url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| aria-label="깃허브 프로필로 이동" | ||
|
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. aria-label, alt 등 접근성 고려한 부분 좋아요! 저도 의식해서 작성하는 습관을 길러봐야겠어요 |
||
| > | ||
| <img src={user.avatar_url} alt="프로필 이미지" css={imageStyle} /> | ||
| </a> | ||
|
|
||
| {user.name && ( | ||
| <a | ||
| href={user.html_url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| css={nameStyle} | ||
| > | ||
| {user.name} | ||
| </a> | ||
| )} | ||
| {user.login && <p css={nameStyle}>{user.login}</p>} | ||
| {user.bio && <p css={nameStyle}>{user.bio}</p>} | ||
|
|
||
| <div css={followInfoWrapper}> | ||
| <div css={followInfoItem}> | ||
| <p>Followers</p> | ||
| <p>{user.followers}</p> | ||
| </div> | ||
| <div css={followInfoItem}> | ||
| <p>Following</p> | ||
| <p>{user.following}</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default GithubCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { css } from '@emotion/react'; | ||
|
|
||
| export const cardStyle = css` | ||
| display: flex; | ||
| position: relative; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| padding: 3rem 6rem; | ||
| gap: 2rem; | ||
|
|
||
| border-radius: 20px; | ||
| background-color: #5f84a2; | ||
| color: #fff; | ||
| `; | ||
|
|
||
| export const closeButtonStyle = css` | ||
| position: absolute; | ||
| top: 1rem; | ||
| right: 1rem; | ||
|
|
||
| border: none; | ||
| font-size: 2rem; | ||
| color: #cadeed; | ||
| background: none; | ||
| cursor: pointer; | ||
| `; | ||
|
|
||
| export const imageStyle = css` | ||
| width: 20rem; | ||
| border-radius: 100%; | ||
| border: 5px solid #cadeed; | ||
| cursor: pointer; | ||
| transition: transform 0.3s ease; | ||
|
|
||
| &:hover { | ||
| transform: scale(1.1); | ||
| } | ||
| `; | ||
|
|
||
| export const nameStyle = css` | ||
| text-align: center; | ||
| font-size: 2rem; | ||
| font-weight: 500; | ||
| color: white; | ||
| text-decoration: none; | ||
| `; | ||
|
|
||
| export const followInfoWrapper = css` | ||
| display: flex; | ||
| justify-content: space-between; | ||
| gap: 2rem; | ||
| `; | ||
|
|
||
| export const followInfoItem = css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| padding: 2rem; | ||
| gap: 1rem; | ||
|
|
||
| font-size: 2rem; | ||
| font-weight: 500; | ||
| border-radius: 5px; | ||
| background-color: #cadeed; | ||
| color: #142755; | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { | ||
| wrapperStyle, | ||
| recentWrapperStyle, | ||
| recentKeywordStyle, | ||
| loadingSpinnerStyle, | ||
| errorMessageStyle, | ||
| } from './GithubSearch.style'; | ||
| import { useGithubSearch } from '../../hooks/useGithubUserInfo'; | ||
| import Input from '../Input/Input'; | ||
| import GithubCard from '../GithubCard/GithubCard'; | ||
|
|
||
| const GithubSearch = () => { | ||
| const { | ||
| input, | ||
| setInput, | ||
| userInfo, | ||
| recent, | ||
| getUserInfo, | ||
| handleKeyDown, | ||
| removeProfile, | ||
| removeRecent, | ||
| } = useGithubSearch(); | ||
|
|
||
| return ( | ||
| <div css={wrapperStyle}> | ||
| {/* 깃허브 검색 */} | ||
| <Input | ||
| value={input} | ||
| onChange={(e) => setInput(e.target.value)} | ||
| onKeyDown={handleKeyDown} | ||
| placeholder="Github 프로필을 검색해보세요." | ||
| /> | ||
|
|
||
| {/* 최근 검색어 */} | ||
| {recent.length > 0 && ( | ||
| <div css={recentWrapperStyle}> | ||
| <h3>최근 검색어</h3> | ||
| <div> | ||
| {recent.map((user) => ( | ||
| <span key={user} css={recentKeywordStyle}> | ||
| <button onClick={() => getUserInfo(user)}>{user}</button> | ||
| <button | ||
| onClick={() => removeRecent(user)} | ||
| aria-label="최근 검색어 삭제" | ||
|
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. 👍👍 |
||
| > | ||
| x | ||
| </button> | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 검색 결과 */} | ||
| {userInfo.status === 'pending' && <div css={loadingSpinnerStyle}></div>} | ||
| {userInfo.status === 'rejected' && ( | ||
| <p css={errorMessageStyle}> | ||
| 결과를 찾을 수 없습니다. 다시 시도해주세요. | ||
| </p> | ||
| )} | ||
| {userInfo.status === 'resolved' && userInfo.data && ( | ||
| <GithubCard user={userInfo.data} onClose={removeProfile} /> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default GithubSearch; | ||
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.
영어로만 상태 지정해봤었는데 한글 문자열로 상태를 지정한 이유가 있을까요?