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-assignment-reopened/.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?
8 changes: 8 additions & 0 deletions week3-assignment-reopened/.prettierrc.json
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
}
12 changes: 12 additions & 0 deletions week3-assignment-reopened/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-assignment-reopened/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 },
],
},
},
]
12 changes: 12 additions & 0 deletions week3-assignment-reopened/index.html
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>
32 changes: 32 additions & 0 deletions week3-assignment-reopened/package.json
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"
}
}
27 changes: 27 additions & 0 deletions week3-assignment-reopened/src/App.jsx
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('깃허브');

Choose a reason for hiding this comment

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

영어로만 상태 지정해봤었는데 한글 문자열로 상태를 지정한 이유가 있을까요?


const tabs = {
깃허브: <GithubSearch />,
야구: <NumberBaseballGame />,
};

return (
<>
<Global styles={globalStyle} />
<Layout header={<Header selectedTab={tab} onChangeTab={setTab} />}>
{tabs[tab]}
</Layout>
</>
);
}

export default App;
54 changes: 54 additions & 0 deletions week3-assignment-reopened/src/components/GithubCard/GithubCard.jsx
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;
Copy link

Choose a reason for hiding this comment

The 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="깃허브 프로필로 이동"

Choose a reason for hiding this comment

The 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="최근 검색어 삭제"
Copy link

Choose a reason for hiding this comment

The 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;
Loading