송용우

Merge commit '89354c77'

Showing 81 changed files with 3951 additions and 0 deletions
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `yarn build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
{
"compilerOptions": {
"target": "es6"
}
}
This diff could not be displayed because it is too large.
{
"name": "jaksimsamil-page",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.10.2",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"axios": "^0.19.2",
"immer": "^7.0.5",
"include-media": "^1.4.9",
"moment": "^2.27.0",
"open-color": "^1.7.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.1.3",
"styled-components": "^5.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:4000"
}
No preview for this file type
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
import React from 'react';
import { Route } from 'react-router-dom';
import './App.css';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import HomePage from './pages/HomePage';
import SettingPage from './pages/SettingPage';
function App() {
return (
<>
<Route component={HomePage} path={['/@:username', '/']} exact />
<Route component={LoginPage} path="/login" />
<Route component={RegisterPage} path="/register" />
<Route component={SettingPage} path="/setting" />
</>
);
}
export default App;
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';
import Button from '../common/Button';
const AuthFormBlock = styled.div`
h3 {
margin: 0;
color: ${palette.gray[8]};
margin-bottom: 1rem;
}
`;
const StyledInput = styled.input`
font-size: 1rem;
border: none;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
width: 100%;
&:focus {
color: $oc-teal-7;
border-bottom: 1px solid ${palette.gray[7]};
}
& + & {
margin-top: 1rem;
}
`;
const Footer = styled.div`
margin-top: 2rem;
text-align: right;
a {
color: ${palette.gray[6]};
text-decoration: underline;
&:hover {
color: ${palette.gray[9]};
}
}
`;
const ButtonWithMarginTop = styled(Button)`
margin-top: 1rem;
`;
const ErrorMessage = styled.div`
color: red;
text-align: center;
font-size: 0.875rem;
margin-top: 1rem;
`;
const textMap = {
login: '로그인',
register: '회원가입',
};
const AuthForm = ({ type, form, onChange, onSubmit, error }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h3>{text}</h3>
<form onSubmit={onSubmit}>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
onChange={onChange}
value={form.username}
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
onChange={onChange}
value={form.password}
/>
{type === 'register' && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
onChange={onChange}
value={form.passwordConfirm}
/>
)}
{error && <ErrorMessage>{error}</ErrorMessage>}
<ButtonWithMarginTop cyan fullWidth>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
/*
register/login Layout
*/
const AuthTemplateBlock = styled.div`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: ${palette.gray[2]};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const WhiteBox = styled.div`
.logo-area {
display: block;
padding-bottom: 2rem;
text-align: center;
font-weight: bold;
letter-spacing: 2px;
}
box-shadow: 0 0 8px rgba(0, 0, 0, 0.025);
padding: 2rem;
width: 360px;
background: white;
border-radius: 2px;
`;
const AuthTemplate = ({ children }) => {
return (
<AuthTemplateBlock>
<WhiteBox>
<div className="logo-area">
<Link to="/">작심삼일</Link>
</div>
{children}
</WhiteBox>
</AuthTemplateBlock>
);
};
export default AuthTemplate;
import React from 'react';
import styled, { css } from 'styled-components';
import palette from '../../lib/styles/palette';
import { withRouter } from 'react-router-dom';
const StyledButton = styled.button`
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
padding: 0.25rem 1rem;
color: white;
outline: none;
cursor: pointer;
background: ${palette.gray[8]};
&:hover {
background: ${palette.gray[6]};
}
${props =>
props.fullWidth &&
css`
padding-top: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
font-size: 1.125rem;
`}
${props =>
props.cyan &&
css`
background: ${palette.cyan[5]};
&:hover {
background: ${palette.cyan[4]};
}
`}
`;
const Button = ({ to, history, ...rest }) => {
const onClick = e => {
if (to) {
history.push(to);
}
if (rest.onClick) {
rest.onClick(e);
}
};
return <StyledButton {...rest} onClick={onClick} />;
};
export default withRouter(Button);
import React from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
const categories = [
{
name: 'home',
text: '홈',
},
{
name: 'setting',
text: '설정',
},
];
const CategoriesBlock = styled.div`
display: flex;
padding: 1rem;
margin: 0 auto;
@media screen and (max-width: 768px) {
width: 100%;
overflow-x: auto;
}
`;
const Category = styled(NavLink)`
font-size: 1.2rem;
cursor: pointer;
white-space: pre;
text-decoration: none;
color: inherit;
padding-bottom: 0.25rem;
&:hover {
color: #495057;
}
& + & {
margin-left: 2rem;
}
&.active {
font-weight: 600;
border-bottom: 2px solid #22b8cf;
color: #22b8cf;
&:hover {
color: #3bc9db;
}
}
`;
const Categories = () => {
return (
<CategoriesBlock>
{categories.map((c) => (
<Category
activeClassName="active"
key={c.name}
exact={c.name === 'home'}
to={c.name === 'home' ? '/' : `/${c.name}`}
>
{c.text}
</Category>
))}
</CategoriesBlock>
);
};
export default Categories;
import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import { Link } from 'react-router-dom';
import Categories from './Categories';
const HeaderBlock = styled.div`
position: fixed;
width: 100%;
background: white;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.125rem;
font-weight: 800;
letter-spacing: 2px;
}
.right {
display: flex;
align-items: center;
}
`;
const Spacer = styled.div`
height: 4rem;
`;
const UserInfo = styled.div`
font-weight: 800;
margin-right: 1rem;
`;
const Header = ({ user, onLogout, category, onSelect }) => {
return (
<>
<HeaderBlock>
<Wrapper>
<Link to="/" className="logo">
작심삼일
</Link>
<Categories
category={category}
onSelect={onSelect}
className="right"
/>
{user ? (
<div className="right">
<UserInfo>{user.username}</UserInfo>
<Button onClick={onLogout}>로그아웃</Button>
</div>
) : (
<div className="right">
<Button to="/login">로그인</Button>
</div>
)}
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
import React from 'react';
import styled from 'styled-components';
const ResponsiveBlock = styled.div`
padding-left: 1rem;
padding-right: 1rem;
width: 1024px;
margin: 0 auto;
@media (max-width: 1024px) {
width: 768px;
}
@media (max-width: 768px) {
width: 100%;
}
`;
const Responsive = ({ children, ...rest }) => {
return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>;
};
export default Responsive;
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import palette from '../../lib/styles/palette';
import AuthForm from '../auth/AuthForm';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
background: palette.gray[2],
padding: theme.spacing(8),
},
paper: {
padding: theme.spacing(8),
margin: 'auto',
textAlign: 'center',
color: theme.palette.text.secondary,
},
}));
const HomeForm = ({ PSdata, goalNum }) => {
const classes = useStyles();
return PSdata ? (
<div className={classes.root}>
<Grid container spacing={5}>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h1>{PSdata.recommend_data.problem_number}</h1>
<h1>{PSdata.recommend_data.problem_title}</h1>
<a
href={'http://www.boj.kr/' + PSdata.recommend_data.problem_number}
>
바로가기
</a>
<h3>오늘의 추천 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1>{PSdata.presentNum + '/' + goalNum}</h1>
<h3>오늘 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1>{PSdata.latestSolve.problem_number}</h1>
<h1>{PSdata.latestSolve.problem_title}</h1>
<h3>마지막으로 문제</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.weekNum}</h1>
<h3>7</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.monthNum}</h1>
<h3>30</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.totalNum}</h1>
<h3>전체</h3>
</Paper>
</Grid>
</Grid>
</div>
) : (
<div className={classes.root}>
<Grid container spacing={5}>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h1></h1>
<h3>오늘의 추천 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1></h1>
<h3>오늘</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1></h1>
<h3>마지막 </h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>7</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>30</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>전체</h3>
</Paper>
</Grid>
</Grid>
</div>
);
};
export default HomeForm;
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => {
const classes = useStyles();
return (
<div>
<form>
<TextField
name="userBJID"
onChange={onChange}
value={profile.userBJID}
placeholder="백준 아이디"
label="백준 아이디"
/>
</form>
<Button
className={classes.button}
variant="outlined"
onClick={onBJIDSubmit}
color="primary"
>
등록
</Button>
<Button
className={classes.button}
variant="outlined"
onClick={onSyncBJIDSubmit}
color="secondary"
>
동기화
</Button>
</div>
);
};
export default BJIDForm;
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const GoalNumForm = ({ onChange, profile, onGoalNumSubmit }) => {
const classes = useStyles();
return (
<div>
<form>
<TextField
name="goalNum"
type="number"
onChange={onChange}
value={profile.goalNum}
placeholder="일일 목표"
label="일일 목표"
InputLabelProps={{
shrink: true,
}}
/>
</form>
<Button
className={classes.button}
onClick={onGoalNumSubmit}
color="primary"
variant="outlined"
>
등록
</Button>
</div>
);
};
export default GoalNumForm;
import React from 'react';
import palette from '../../lib/styles/palette';
import BJIDForm from './BJIDForm';
import SlackForm from './SlackForm';
import GoalNumForm from './GoalNumForm';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import CircularProgress from '@material-ui/core/CircularProgress';
import styled from 'styled-components';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
background: palette.gray[2],
padding: theme.spacing(8),
},
paper: {
padding: theme.spacing(8),
margin: 'auto',
textAlign: 'center',
},
}));
const LoadingParentStyle = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20px;
`;
const SettingForm = ({
onChange,
onBJIDSubmit,
onSlackURLSubmit,
profile,
onSyncBJIDSubmit,
onGoalNumSubmit,
isLoading,
}) => {
const classes = useStyles();
return isLoading ? (
<LoadingParentStyle>
<CircularProgress className={classes.loading} />
</LoadingParentStyle>
) : (
<div className={classes.root}>
<Grid container spacing={5}>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>백준 아이디</h1>
<BJIDForm
profile={profile}
onChange={onChange}
onBJIDSubmit={onBJIDSubmit}
onSyncBJIDSubmit={onSyncBJIDSubmit}
/>
</Paper>
</Grid>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>슬랙 Hook URL</h1>
<SlackForm
profile={profile}
onChange={onChange}
onSlackURLSubmit={onSlackURLSubmit}
/>
</Paper>
</Grid>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>일일 목표</h1>
<GoalNumForm
profile={profile}
onChange={onChange}
onGoalNumSubmit={onGoalNumSubmit}
/>
</Paper>
</Grid>
</Grid>
</div>
);
};
export default SettingForm;
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const SlackForm = ({ onChange, profile, onSlackURLSubmit }) => {
const classes = useStyles();
return (
<div>
<form>
<TextField
name="slackWebHookURL"
onChange={onChange}
value={profile.slackWebHookURL}
placeholder="슬랙 Webhook URL"
label="슬랙 Webhook URL"
/>
</form>
<Button
className={classes.button}
onSubmit={onSlackURLSubmit}
variant="outlined"
type="submit"
color="primary"
>
등록
</Button>
</div>
);
};
export default SlackForm;
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { changeField, initializeForm, login } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';
const LoginForm = ({ history }) => {
const dispatch = useDispatch();
const [error, setError] = useState(null);
const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
form: auth.login,
auth: auth.auth,
authError: auth.authError,
user: user.user,
}));
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'login',
key: name,
value,
}),
);
};
const onSubmit = (e) => {
e.preventDefault();
const { username, password } = form;
dispatch(login({ username, password }));
};
useEffect(() => {
dispatch(initializeForm('login'));
}, [dispatch]);
useEffect(() => {
if (authError) {
console.log('Error Occured');
console.log(authError);
setError('로그인 실패');
return;
}
if (auth) {
console.log('Login Success');
dispatch(check());
}
}, [auth, authError, dispatch]);
useEffect(() => {
if (user) {
history.push('/');
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('localStorage is not working');
}
console.log(user);
}
}, [history, user]);
return (
<AuthForm
type="login"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
></AuthForm>
);
};
export default withRouter(LoginForm);
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';
import { withRouter } from 'react-router-dom';
const RegisterForm = ({ history }) => {
const [error, setError] = useState(null);
const dispatch = useDispatch();
const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
form: auth.register,
auth: auth.auth,
authError: auth.authError,
user: user.user,
}));
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'register',
key: name,
value,
}),
);
};
const onSubmit = (e) => {
e.preventDefault();
const { username, password, passwordConfirm } = form;
if ([username, password, passwordConfirm].includes('')) {
setError('빈 칸을 모두 입력하세요');
return;
}
if (password !== passwordConfirm) {
setError('비밀번호가 일치하지 않습니다.');
changeField({ form: 'register', key: 'password', value: '' });
changeField({ form: 'register', key: 'passwordConfirm', value: '' });
return;
}
dispatch(register({ username, password }));
};
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
useEffect(() => {
if (authError) {
if (authError.response.status === 409) {
setError('이미 존재하는 계정명입니다.');
return;
}
setError('회원가입 실패');
return;
}
if (auth) {
console.log('Register Success!');
console.log(auth);
dispatch(check());
}
}, [auth, authError, dispatch]);
useEffect(() => {
if (user) {
console.log('SUCCESS check API');
history.push('/');
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('localStorage is not working');
}
}
}, [history, user]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
></AuthForm>
);
};
export default withRouter(RegisterForm);
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Header from '../../components/common/Header';
import { logout } from '../../modules/user';
const HeaderContainer = () => {
const { user } = useSelector(({ user }) => ({ user: user.user }));
const dispatch = useDispatch();
const onLogout = () => {
dispatch(logout());
};
return <Header user={user} onLogout={onLogout} />;
};
export default HeaderContainer;
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import HomeForm from '../../components/home/HomeForm';
import { getPROFILE, initializeProfile } from '../../modules/profile';
const HomeContainer = ({ history }) => {
const dispatch = useDispatch();
const { user, profile } = useSelector(({ user, profile }) => ({
user: user.user,
profile: profile,
}));
useEffect(() => {
if (!user) {
alert('로그인이 필요합니다 ');
history.push('/login');
} else {
let username = user.username;
dispatch(getPROFILE({ username }));
return () => {
dispatch(initializeProfile());
};
}
}, [dispatch, user, history]);
useEffect(() => {
console.log(profile);
}, [profile]);
useEffect(() => {
if (user) {
let username = user.username;
dispatch(getPROFILE({ username }));
}
}, [dispatch, user]);
return <HomeForm PSdata={profile.solvedBJ_date} goalNum={profile.goalNum} />;
};
export default withRouter(HomeContainer);
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {
changeField,
setBJID,
getPROFILE,
syncBJID,
initializeProfile,
setSLACK,
setGOALNUM,
} from '../../modules/profile';
import SettingForm from '../../components/setting/SettingForm';
const SettingContainer = ({ history }) => {
const [isLoading, setLoading] = useState(false);
const dispatch = useDispatch();
const { user, profile, loading } = useSelector(
({ user, profile, loading }) => ({
user: user.user,
profile: profile,
loading: loading,
}),
);
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
key: name,
value: value,
}),
);
};
const onSyncBJIDSubmit = (e) => {
e.preventDefault();
let username = profile.username;
dispatch(syncBJID({ username }));
};
const onGoalNumSubmit = (e) => {
e.preventDefault();
let username = profile.username;
let goalNum = profile.goalNum;
dispatch(setGOALNUM({ username, goalNum }));
};
const onSlackURLSubmit = (e) => {
e.preventDefault();
let username = profile.username;
let slackWebHookURL = profile.slackWebHookURL;
dispatch(setSLACK({ username, slackWebHookURL }));
};
const onBJIDSubmit = (e) => {
e.preventDefault();
let username = profile.username;
let userBJID = profile.userBJID;
dispatch(setBJID({ username, userBJID }));
};
useEffect(() => {
if (!user) {
alert('로그인이 필요합니다 ');
history.push('/login');
} else {
let username = user.username;
dispatch(getPROFILE({ username }));
return () => {
dispatch(initializeProfile());
};
}
}, [dispatch, user, history]);
useEffect(() => {
if (loading['profile/SYNC_BJID'] == true) {
setLoading(true);
} else {
setLoading(false);
}
}, [dispatch, loading]);
return (
<div>
<SettingForm
type="setting"
onChange={onChange}
onBJIDSubmit={onBJIDSubmit}
onSyncBJIDSubmit={onSyncBJIDSubmit}
onSlackURLSubmit={onSlackURLSubmit}
onGoalNumSubmit={onGoalNumSubmit}
profile={profile}
isLoading={isLoading}
></SettingForm>
</div>
);
};
export default withRouter(SettingContainer);
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
min-height: 100%;
}
#root {
min-height: 100%;
}
html {
height: 100%;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: inherit;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)),
);
function loadUser() {
try {
const user = localStorage.getItem('user');
if (!user) return;
store.dispatch(tempSetUser(user));
store.dispatch(check());
} catch (e) {
console.log('localStorage is not working');
}
}
sagaMiddleware.run(rootSaga);
loadUser();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
import client from './client';
export const login = ({ username, password }) =>
client.post('api/auth/login', { username, password });
export const register = ({ username, password }) =>
client.post('api/auth/register', { username, password });
export const check = () => client.get('api/auth/check');
export const logout = () => client.post('/api/auth/logout');
import axios from 'axios';
const client = axios.create();
export default client;
import client from './client';
export const setBJID = ({ username, userBJID }) =>
client.post('api/profile/setprofile', {
username: username,
userBJID: userBJID,
});
export const setPROFILE = (postdata) =>
client.post('api/profile/setprofile', postdata);
export const getPROFILE = ({ username }) =>
client.post('api/profile/getprofile', { username });
export const syncBJ = ({ username }) =>
client.patch('api/profile/syncBJ', { username });
import { call, put } from 'redux-saga/effects';
import { startLoading, finishLoading } from '../modules/loading';
export const createRequestActionTypes = (type) => {
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return [type, SUCCESS, FAILURE];
};
export default function createRequestSaga(type, request) {
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return function* (action) {
yield put(startLoading(type));
try {
const response = yield call(request, action.payload);
yield put({
type: SUCCESS,
payload: response.data,
});
} catch (e) {
yield put({
type: FAILURE,
payload: e,
error: true,
});
}
yield put(finishLoading(type));
};
}
// source: https://yeun.github.io/open-color/
const palette = {
gray: [
'#f8f9fa',
'#f1f3f5',
'#e9ecef',
'#dee2e6',
'#ced4da',
'#adb5bd',
'#868e96',
'#495057',
'#343a40',
'#212529',
],
cyan: [
'#e3fafc',
'#c5f6fa',
'#99e9f2',
'#66d9e8',
'#3bc9db',
'#22b8cf',
'#15aabf',
'#1098ad',
'#0c8599',
'#0b7285',
],
};
export default palette;
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
import { takeLatest } from 'redux-saga/effects';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/createRequestSaga';
import * as authAPI from '../lib/api/auth';
const CHANGE_FIELD = 'auth/CHANGE_FIELD';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';
const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] = createRequestActionTypes(
'auth/REGISTER',
);
const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] = createRequestActionTypes(
'auth/REGISTER',
);
export const changeField = createAction(
CHANGE_FIELD,
({ form, key, value }) => ({
form,
key,
value,
}),
);
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);
const initalState = {
register: {
username: '',
password: '',
passwordConfirm: '',
},
login: {
username: '',
password: '',
},
auth: null,
authError: null,
};
export const register = createAction(REGISTER, ({ username, password }) => ({
username,
password,
}));
export const login = createAction(LOGIN, ({ username, password }) => ({
username,
password,
}));
const registerSaga = createRequestSaga(REGISTER, authAPI.register);
const loginSaga = createRequestSaga(LOGIN, authAPI.login);
export function* authSaga() {
yield takeLatest(REGISTER, registerSaga);
yield takeLatest(LOGIN, loginSaga);
}
const auth = handleActions(
{
[CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
produce(state, (draft) => {
draft[form][key] = value;
}),
[INITIALIZE_FORM]: (state, { payload: form }) => ({
...state,
[form]: initalState[form],
authError: null,
}),
[REGISTER_SUCCESS]: (state, { payload: auth }) => ({
...state,
authError: null,
auth,
}),
[REGISTER_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
[LOGIN_SUCCESS]: (state, { payload: auth }) => ({
...state,
authError: null,
auth,
}),
[LOGIN_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
},
initalState,
);
export default auth;
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import loading from './loading';
import user, { userSaga } from './user';
import profile, { profileSaga } from './profile';
const rootReducer = combineReducers({
auth,
loading,
user,
profile,
});
export function* rootSaga() {
yield all([authSaga(), userSaga(), profileSaga()]);
}
export default rootReducer;
import { createAction, handleActions } from 'redux-actions';
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
export const startLoading = createAction(
START_LOADING,
(requestType) => requestType,
);
export const finishLoading = createAction(
FINISH_LOADING,
(requestType) => requestType,
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true,
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false,
}),
},
initialState,
);
export default loading;
import { createAction, handleActions } from 'redux-actions';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/createRequestSaga';
import produce from 'immer';
import * as profileAPI from '../lib/api/profile';
import { takeLatest } from 'redux-saga/effects';
const INITIALIZE = 'profile/INITIALIZE';
const CHANGE_FIELD = 'profile/CHANGE_FIELD';
const [SET_BJID, SET_BJID_SUCCESS, SET_BJID_FAILURE] = createRequestActionTypes(
'profile/SET_BJID',
);
const [
SET_SLACK,
SET_SLACK_SUCCESS,
SET_SLACK_FAILURE,
] = createRequestActionTypes('/profile/SET_SLACK');
const [
SET_GOALNUM,
SET_GOALNUM_SUCCESS,
SET_GOALNUM_FAILURE,
] = createRequestActionTypes('/profile/SET_GOALNUM');
const [
GET_PROFILE,
GET_PROFILE_SUCCESS,
GET_PROFILE_FAILURE,
] = createRequestActionTypes('profile/GET_PROFILE');
const [
SYNC_BJID,
SYNC_BJID_SUCCESS,
SYNC_BJID_FAILURE,
] = createRequestActionTypes('profile/SYNC_BJID');
export const initializeProfile = createAction(INITIALIZE);
export const syncBJID = createAction(SYNC_BJID, ({ username }) => ({
username,
}));
export const setSLACK = createAction(
SET_SLACK,
({ username, slackWebHookURL }) => ({
username,
slackWebHookURL,
}),
);
export const setGOALNUM = createAction(
SET_GOALNUM,
({ username, goalNum }) => ({
username,
goalNum,
}),
);
export const setBJID = createAction(SET_BJID, ({ username, userBJID }) => ({
username,
userBJID,
}));
export const changeField = createAction(CHANGE_FIELD, ({ key, value }) => ({
key,
value,
}));
export const getPROFILE = createAction(GET_PROFILE, ({ username }) => ({
username,
}));
const initialState = {
username: '',
userBJID: '',
solvedBJ: '',
friendList: [],
profileError: '',
slackWebHookURL: '',
solvedBJ_date: '',
goalNum: '',
};
const getPROFILESaga = createRequestSaga(GET_PROFILE, profileAPI.getPROFILE);
const setBJIDSaga = createRequestSaga(SET_BJID, profileAPI.setBJID);
const setSLACKSaga = createRequestSaga(SET_SLACK, profileAPI.setPROFILE);
const setGOALNUMSaga = createRequestSaga(SET_GOALNUM, profileAPI.setPROFILE);
const syncBJIDSaga = createRequestSaga(SYNC_BJID, profileAPI.syncBJ);
export function* profileSaga() {
yield takeLatest(SET_BJID, setBJIDSaga);
yield takeLatest(GET_PROFILE, getPROFILESaga);
yield takeLatest(SYNC_BJID, syncBJIDSaga);
yield takeLatest(SET_SLACK, setSLACKSaga);
yield takeLatest(SET_GOALNUM, setGOALNUMSaga);
}
export default handleActions(
{
[INITIALIZE]: (state) => initialState,
[CHANGE_FIELD]: (state, { payload: { key, value } }) =>
produce(state, (draft) => {
draft[key] = value;
}),
[GET_PROFILE_SUCCESS]: (
state,
{
payload: {
username,
userBJID,
solvedBJ,
friendList,
slackWebHookURL,
solvedBJ_date,
goalNum,
},
},
) => ({
...state,
username: username,
userBJID: userBJID,
solvedBJ: solvedBJ,
friendList: friendList,
profileError: null,
slackWebHookURL: slackWebHookURL,
solvedBJ_date: solvedBJ_date,
goalNum: goalNum,
}),
[GET_PROFILE_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
[SET_BJID_SUCCESS]: (state, { payload: { userBJID } }) => ({
...state,
userBJID: userBJID,
profileError: null,
}),
[SET_BJID_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
[SET_SLACK_SUCCESS]: (state, { payload: { slackWebHookURL } }) => ({
...state,
slackWebHookURL: slackWebHookURL,
profileError: null,
}),
[SET_SLACK_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
[SET_GOALNUM_SUCCESS]: (state, { payload: { goalNum } }) => ({
...state,
goalNum: goalNum,
}),
[SET_GOALNUM_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
[SYNC_BJID_SUCCESS]: (state, { payload: { solvedBJ } }) => ({
...state,
solvedBJ,
profileError: null,
}),
[SYNC_BJID_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
},
initialState,
);
import { createAction, handleActions } from 'redux-actions';
import { takeLatest, call } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/createRequestSaga';
const TEMP_SET_USER = 'user/TEMP_SET_USER';
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] = createRequestActionTypes(
'user/CHECK',
);
const LOGOUT = 'user/LOGOUT';
export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);
export const logout = createAction(LOGOUT);
const checkSaga = createRequestSaga(CHECK, authAPI.check);
function checkFailureSaga() {
try {
localStorage.removeItem('user');
} catch (e) {
console.log('localStroage is not working');
}
}
function* logoutSaga() {
try {
yield call(authAPI.logout);
console.log('logout');
localStorage.removeItem('user');
} catch (e) {
console.log(e);
}
}
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
yield takeLatest(CHECK_FAILURE, checkFailureSaga);
yield takeLatest(LOGOUT, logoutSaga);
}
const initialState = {
user: null,
checkError: null,
};
export default handleActions(
{
[TEMP_SET_USER]: (state, { payload: user }) => ({
...state,
user,
}),
[CHECK_SUCCESS]: (state, { payload: user }) => ({
...state,
user,
checkError: null,
}),
[CHECK_FAILURE]: (state, { payload: error }) => ({
...state,
user: null,
checkError: error,
}),
[LOGOUT]: (state) => ({
...state,
user: null,
}),
},
initialState,
);
import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';
import HomeContainer from '../containers/home/HomeContainer';
const HomePage = () => {
return (
<div>
<HeaderContainer />
<HomeContainer />
</div>
);
};
export default HomePage;
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import LoginForm from '../containers/auth/LoginForm';
const LoginPage = () => {
return (
<AuthTemplate>
<LoginForm type="login" />
</AuthTemplate>
);
};
export default LoginPage;
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import RegisterForm from '../containers/auth/RegisterForm';
const RegisterPage = () => {
return (
<AuthTemplate>
<RegisterForm type="register" />
</AuthTemplate>
);
};
export default RegisterPage;
import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';
import SettingContainer from '../containers/setting/SettingContainer';
const SettingPage = () => {
return (
<div>
<HeaderContainer />
<SettingContainer></SettingContainer>
</div>
);
};
export default SettingPage;
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
This diff could not be displayed because it is too large.
{
"parser": "babel-eslint",
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11
},
"rules": {}
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dotenv
.env
#access.log
access.log
# dependencies
/node_modules
# Jaksimsamil API Documentation
## Overview
- TBA
## URL
- TBA
## Usage
- TBA
## Example
- TBA
## API Table
| group | description | method | URL | Detail | Auth |
| ------- | --------------------------------- | ------ | ----------------------- | -------- | --------- |
| user | 유저 등록 | POST | api/user | 바로가기 | JWT Token |
| user | 유저 삭제 | DELETE | api/user:id | 바로가기 | JWT Token |
| user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None |
| user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token |
| friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token |
| friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None |
| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | 바로가기 | None |
| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | 바로가기 | None |
| profile | 유저 정보 수정 | POST | api/profile/setprofile | 바로가기 | JWT TOKEN |
| profile | 유저 정보 받아오기 | POST | api/profile/getprofile | 바로가기 | JWT |
| profile | 추천 문제 조회 | POST | api/profile/recommend | 바로가기 | None |
| notify | 슬랙 메시지 전송 요청 (성취여부) | POST | api/notify/goal | 바로가기 | Jwt Token |
| notify | 슬랙 메시지 전송 요청 (문제 추천) | POST | api/notify/recommend | 바로가기 | None |
| auth | 로그인 | POST | api/auth/login | 바로가기 | None |
| auth | 로그아웃 | POST | api/auth/logout | 바로가기 | JWT Token |
| auth | 회원가입 | POST | api/auth/register | 바로가기 | None |
| auth | 로그인 확인 | GET | api/auth/check | 바로가기 | None |
This diff could not be displayed because it is too large.
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const mongoose = require("mongoose");
const fs = require("fs");
const morgan = require("koa-morgan");
const jwtMiddleware = require("./src/lib/jwtMiddleware");
const api = require("./src/api");
require("dotenv").config();
const app = new Koa();
const router = new Router();
const accessLogStream = fs.createWriteStream(__dirname + "/access.log", {
flags: "a",
});
require("dotenv").config();
app.use(bodyParser());
app.use(jwtMiddleware);
app.use(morgan("combined", { stream: accessLogStream }));
const { SERVER_PORT, MONGO_URL } = process.env;
router.use("/api", api.routes());
app.use(router.routes()).use(router.allowedMethods());
mongoose
.connect(MONGO_URL, {
useNewUrlParser: true,
useFindAndModify: false,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to MongoDB");
})
.catch((e) => {
console.log(e);
});
app.listen(SERVER_PORT, () => {
console.log("Server is running on port", process.env.SERVER_PORT);
});
This diff could not be displayed because it is too large.
{
"name": "jaksimsamil-server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"axios": "^0.19.2",
"bcrypt": "^4.0.1",
"body-parser": "^1.19.0",
"cheerio": "^1.0.0-rc.3",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"eslint-config-prettier": "^6.11.0",
"fs": "^0.0.1-security",
"iconv": "^3.0.0",
"joi": "^14.3.1",
"jsonwebtoken": "^8.5.1",
"koa": "^2.12.0",
"koa-bodyparser": "^4.3.0",
"koa-morgan": "^1.0.1",
"koa-router": "^9.0.1",
"mongoose": "^5.9.17",
"morgan": "^1.10.0",
"node-schedule": "^1.3.2",
"path": "^0.12.7",
"slack-client": "^2.0.6",
"slack-node": "^0.1.8",
"voca": "^1.4.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.1.0",
"nodemon": "^2.0.4"
},
"scripts": {
"start": "node src",
"start:dev": "nodemon --watch src/ src/index.js"
}
}
const Joi = require("joi");
const User = require("../../models/user");
const Profile = require("../../models/profile");
/*
POST /api/auth/register
{
username: 'userid'
password: 'userpassword'
}
*/
exports.register = async (ctx) => {
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(20).required(),
password: Joi.string().required(),
});
const result = Joi.validate(ctx.request.body, schema);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
const { username, password } = ctx.request.body;
try {
const isNameExist = await User.findByUsername(username);
if (isNameExist) {
ctx.status = 409;
return;
}
const profile = new Profile({
username,
});
const user = new User({
username,
});
await user.setPassword(password);
await profile.save();
await user.save();
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set("access_token", token, {
//3일동안 유효
maxAge: 1000 * 60 * 60 * 24 * 3,
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST /api/auth/login
{
username: 'userid'
password: 'userpassword'
}
*/
exports.login = async (ctx) => {
const { username, password } = ctx.request.body;
if (!username || !password) {
ctx.status = 401;
return;
}
try {
const user = await User.findByUsername(username);
if (!user) {
ctx.status = 401;
return;
}
const isPasswordValid = await user.checkPassword(password);
if (!isPasswordValid) {
ctx.status = 401;
return;
}
ctx.body = user.serialize();
const token = user.generateToken();
ctx.cookies.set("access_token", token, {
//7일동안 유효
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true,
});
} catch (e) {
ctx.throw(500, e);
}
};
/*
GET api/auth/check
*/
exports.check = async (ctx) => {
const { user } = ctx.state;
if (!user) {
ctx.status = 401;
return;
}
ctx.body = user;
};
/*
POST /api/auth/logout
*/
exports.logout = async (ctx) => {
ctx.cookies.set("access_token");
ctx.status = 204;
};
const Router = require("koa-router");
const auth = new Router();
const authCtrl = require("./auth.ctrl");
auth.post("/login", authCtrl.login);
auth.post("/logout", authCtrl.logout);
auth.post("/register", authCtrl.register);
auth.get("/check", authCtrl.check);
module.exports = auth;
const Router = require("koa-router");
const friend = new Router();
friend.post("/");
friend.delete("/:id");
friend.get("/:id");
friend.get("");
module.exports = friend;
const Router = require("koa-router");
const api = new Router();
const auth = require("./auth");
const friend = require("./friend");
const notify = require("./notify");
const user = require("./user");
const profile = require("./profile");
api.use("/auth", auth.routes());
api.use("/friend", friend.routes());
api.use("/notify", notify.routes());
api.use("/user", user.routes());
api.use("/profile", profile.routes());
module.exports = api;
const Router = require("koa-router");
const notify = new Router();
const slackCtrl = require("./slack.ctrl");
notify.post("/slack/goal", slackCtrl.slackGoal);
notify.post("/slack/recommend", slackCtrl.slackRecommend);
module.exports = notify;
const Profile = require("../../models/profile");
const sendSlack = require("../../util/sendSlack");
const problem_set = require("../../data/problem_set");
const compareBJ = require("../../util/compareBJ");
/*
POST api/notify/slack/goal
{
username: "username"
}
*/
exports.slackGoal = async (ctx) => {
try {
const { username } = ctx.request.body;
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let slackURL = profile.getslackURL();
if (!slackURL) {
ctx.status = 401;
return;
}
let goalNum = profile.getgoalNum();
let todayNum = profile.getTodaySovled();
let message = "";
if (goalNum < todayNum) {
message =
"오늘의 목표 " +
goalNum +
"문제 중 " +
todayNum +
"문제를 풀었습니다." +
"\n" +
"잘하셨습니다!";
} else {
message =
"오늘의 목표 " +
goalNum +
"문제 중 " +
todayNum +
"문제를 풀었습니다." +
"\n" +
"분발하세요!";
}
sendSlack.send(message, slackURL);
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST api/notify/slack/recommend
{
username: "username"
}
*/
exports.slackRecommend = async (ctx) => {
try {
console.log("1");
const { username } = ctx.request.body;
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let slackURL = profile.getslackURL();
if (!slackURL) {
ctx.status = 401;
return;
}
let unsolved_data = compareBJ.compareBJ(
profile.getBJdata(),
problem_set.problem_set
);
let recommendData = compareBJ.randomItem(unsolved_data);
if (!recommendData) {
ctx.status = 401;
return;
}
let message =
"오늘의 추천 문제는 " +
recommendData.problem_number +
"번 " +
" <https://www.boj.kr/" +
recommendData.problem_number +
"|" +
recommendData.problem_title +
">" +
" 입니다.";
sendSlack.send(message, slackURL);
} catch (e) {
ctx.throw(500, e);
}
};
const Router = require("koa-router");
const profile = new Router();
const profileCtrl = require("./profile.ctrl");
profile.post("/solved:id");
profile.get("/solvednum:id");
profile.post("/recommend", profileCtrl.recommend);
profile.patch("/syncBJ", profileCtrl.syncBJ);
profile.post("/setprofile", profileCtrl.setProfile);
profile.post("/getprofile", profileCtrl.getProfile);
module.exports = profile;
const Profile = require("../../models/profile");
const mongoose = require("mongoose");
const getBJ = require("../../util/getBJ");
const Joi = require("joi");
const analyzeBJ = require("../../util/analyzeBJ");
const compareBJ = require("../../util/compareBJ");
const problem_set = require("../../data/problem_set");
const { ObjectId } = mongoose.Types;
exports.checkObjectId = (ctx, next) => {
const { username } = ctx.request.body;
if (!ObjectId.isValid(username)) {
ctx.status = 400;
return;
}
return next();
};
/*POST /api/profile/getprofile
{
username: "username"
}
*/
exports.getProfile = async (ctx) => {
try {
const { username } = ctx.request.body;
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
ctx.body = profile;
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST /api/proflie/setprofile
{
username: "username",
userBJID: "userBJID",
friendList: [String],
}
*/
exports.setProfile = async (ctx) => {
const schema = Joi.object()
.keys({
username: Joi.string(),
userBJID: Joi.string(),
//freindList: Joi.array().items(Joi.string()),
})
.unknown();
console.log(ctx.request.body);
const result = Joi.validate(ctx.request.body, schema);
if (result.error) {
ctx.status = 400;
ctx.body = result.error;
return;
}
try {
const profile = await Profile.findOneAndUpdate(
{ username: ctx.request.body.username },
ctx.request.body,
{
new: true,
}
).exec();
if (!profile) {
ctx.status = 404;
return;
}
ctx.body = profile;
} catch (e) {
ctx.throw(500, e);
}
};
/*
PATCH /api/proflie/syncBJ
{
username: 'userid'
}
*/
exports.syncBJ = async function (ctx) {
const { username } = ctx.request.body;
if (!username) {
ctx.status = 401;
return;
}
try {
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
const BJID = await profile.getBJID();
let BJdata = await getBJ.getBJ(BJID);
let BJdata_date = await analyzeBJ.analyzeBJ(BJdata);
const updateprofile = await Profile.findOneAndUpdate(
{ username: username },
{ solvedBJ: BJdata, solvedBJ_date: BJdata_date },
{ new: true }
).exec();
ctx.body = updateprofile;
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST /api/proflie/recommend
{
username: 'userid'
}
*/
exports.recommend = async (ctx) => {
const { username } = ctx.request.body;
if (!username) {
ctx.status = 401;
return;
}
try {
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let unsolved_data = compareBJ.compareBJ(
profile.getBJdata(),
problem_set.problem_set
);
ctx.body = compareBJ.randomItem(unsolved_data);
//데이터가 비었을 떄 예외처리 필요
} catch (e) {
ctx.throw(500, e);
}
};
const Router = require("koa-router");
const user = new Router();
user.post("/");
user.delete("/:id");
user.get("/:id");
user.get("");
module.exports = user;
exports.problem_set = [
{
problem_number: "1517",
problem_title: "버블 소트",
solved_date: "20200621",
},
{
problem_number: "2448",
problem_title: "별 찍기 - 11",
solved_date: "20200621",
},
{ problem_number: "1891", problem_title: "사분면", solved_date: "20200621" },
{ problem_number: "1074", problem_title: "Z", solved_date: "20200620" },
{
problem_number: "2263",
problem_title: "트리의 순회",
solved_date: "20200620",
},
{
problem_number: "1780",
problem_title: "종이의 개수",
solved_date: "20200619",
},
{
problem_number: "11728",
problem_title: "배열 합치기",
solved_date: "20200619",
},
{
problem_number: "10816",
problem_title: "숫자 카드 2",
solved_date: "20200619",
},
{
problem_number: "10815",
problem_title: "숫자 카드",
solved_date: "20200619",
},
{
problem_number: "2109",
problem_title: "순회강연",
solved_date: "20200619",
},
{
problem_number: "1202",
problem_title: "보석 도둑",
solved_date: "20200619",
},
{
problem_number: "1285",
problem_title: "동전 뒤집기",
solved_date: "20200617",
},
{
problem_number: "2138",
problem_title: "전구와 스위치",
solved_date: "20200617",
},
{ problem_number: "1080", problem_title: "행렬", solved_date: "20200617" },
{ problem_number: "11399", problem_title: "ATM", solved_date: "20200616" },
{
problem_number: "1931",
problem_title: "회의실배정",
solved_date: "20200616",
},
{ problem_number: "11047", problem_title: "동전 0", solved_date: "20200615" },
{
problem_number: "15666",
problem_title: "N과 M (12)",
solved_date: "20200614",
},
{
problem_number: "15665",
problem_title: "N과 M (11)",
solved_date: "20200614",
},
{
problem_number: "15664",
problem_title: "N과 M (10)",
solved_date: "20200614",
},
{
problem_number: "15663",
problem_title: "N과 M (9)",
solved_date: "20200614",
},
{
problem_number: "15657",
problem_title: "N과 M (8)",
solved_date: "20200614",
},
{
problem_number: "15656",
problem_title: "N과 M (7)",
solved_date: "20200614",
},
{
problem_number: "15655",
problem_title: "N과 M (6)",
solved_date: "20200614",
},
{
problem_number: "15654",
problem_title: "N과 M (5)",
solved_date: "20200614",
},
{
problem_number: "15652",
problem_title: "N과 M (4)",
solved_date: "20200614",
},
{
problem_number: "15651",
problem_title: "N과 M (3)",
solved_date: "20200612",
},
{
problem_number: "15650",
problem_title: "N과 M (2)",
solved_date: "20200612",
},
{
problem_number: "15649",
problem_title: "N과 M (1)",
solved_date: "20200612",
},
{ problem_number: "6603", problem_title: "로또", solved_date: "20200612" },
{
problem_number: "10971",
problem_title: "외판원 순회 2",
solved_date: "20200611",
},
{
problem_number: "10819",
problem_title: "차이를 최대로",
solved_date: "20200611",
},
{
problem_number: "10973",
problem_title: "이전 순열",
solved_date: "20200611",
},
{
problem_number: "10974",
problem_title: "모든 순열",
solved_date: "20200611",
},
{
problem_number: "10972",
problem_title: "다음 순열",
solved_date: "20200611",
},
{ problem_number: "7576", problem_title: "토마토", solved_date: "20200608" },
{ problem_number: "1248", problem_title: "맞춰봐", solved_date: "20200312" },
{ problem_number: "2529", problem_title: "부등호", solved_date: "20200311" },
{
problem_number: "15661",
problem_title: "링크와 스타트",
solved_date: "20200311",
},
{ problem_number: "14501", problem_title: "퇴사", solved_date: "20200311" },
{
problem_number: "1759",
problem_title: "암호 만들기",
solved_date: "20200310",
},
{
problem_number: "14391",
problem_title: "종이 조각",
solved_date: "20200306",
},
{
problem_number: "14889",
problem_title: "스타트와 링크",
solved_date: "20200305",
},
{
problem_number: "1182",
problem_title: "부분수열의 합",
solved_date: "20200305",
},
{ problem_number: "11723", problem_title: "집합", solved_date: "20200305" },
{
problem_number: "1748",
problem_title: "수 이어 쓰기 1",
solved_date: "20200305",
},
{
problem_number: "6064",
problem_title: "카잉 달력",
solved_date: "20200305",
},
{ problem_number: "1107", problem_title: "리모컨", solved_date: "20200305" },
{
problem_number: "3085",
problem_title: "사탕 게임",
solved_date: "20200305",
},
{
problem_number: "2309",
problem_title: "일곱 난쟁이",
solved_date: "20200305",
},
{
problem_number: "1748",
problem_title: "수 이어 쓰기 1",
solved_date: "20200228",
},
{
problem_number: "14500",
problem_title: "테트로미노",
solved_date: "20200228",
},
{ problem_number: "1107", problem_title: "리모컨", solved_date: "20200228" },
{
problem_number: "1476",
problem_title: "날짜 계산",
solved_date: "20200228",
},
{
problem_number: "3085",
problem_title: "사탕 게임",
solved_date: "20200228",
},
{
problem_number: "2309",
problem_title: "일곱 난쟁이",
solved_date: "20200228",
},
{
problem_number: "1261",
problem_title: "알고스팟",
solved_date: "20200228",
},
{
problem_number: "13549",
problem_title: "숨바꼭질 3",
solved_date: "20200227",
},
{
problem_number: "14226",
problem_title: "이모티콘",
solved_date: "20200227",
},
{
problem_number: "13913",
problem_title: "숨바꼭질 4",
solved_date: "20200227",
},
{
problem_number: "1697",
problem_title: "숨바꼭질",
solved_date: "20200227",
},
{
problem_number: "1967",
problem_title: "트리의 지름",
solved_date: "20200221",
},
{
problem_number: "1167",
problem_title: "트리의 지름",
solved_date: "20200221",
},
{
problem_number: "11725",
problem_title: "트리의 부모 찾기",
solved_date: "20200221",
},
{
problem_number: "2250",
problem_title: "트리의 높이와 너비",
solved_date: "20200221",
},
{
problem_number: "1991",
problem_title: "트리 순회",
solved_date: "20200221",
},
{
problem_number: "7562",
problem_title: "나이트의 이동",
solved_date: "20200220",
},
{
problem_number: "2178",
problem_title: "미로 탐색",
solved_date: "20200220",
},
{
problem_number: "4963",
problem_title: "섬의 개수",
solved_date: "20200219",
},
{
problem_number: "2667",
problem_title: "단지번호붙이기",
solved_date: "20200219",
},
{
problem_number: "1707",
problem_title: "이분 그래프",
solved_date: "20200217",
},
{
problem_number: "11724",
problem_title: "연결 요소의 개수",
solved_date: "20200214",
},
{
problem_number: "1260",
problem_title: "DFS와 BFS",
solved_date: "20200214",
},
{ problem_number: "13023", problem_title: "ABCDE", solved_date: "20200213" },
{ problem_number: "11652", problem_title: "카드", solved_date: "20200210" },
{
problem_number: "1377",
problem_title: "버블 소트",
solved_date: "20200210",
},
{
problem_number: "11004",
problem_title: "K번째 수",
solved_date: "20200210",
},
{ problem_number: "10825", problem_title: "국영수", solved_date: "20200210" },
{
problem_number: "2751",
problem_title: "수 정렬하기 2",
solved_date: "20200210",
},
{
problem_number: "9461",
problem_title: "파도반 수열",
solved_date: "20200205",
},
{
problem_number: "1699",
problem_title: "제곱수의 합",
solved_date: "20200205",
},
{
problem_number: "9095",
problem_title: "1, 2, 3 더하기",
solved_date: "20200205",
},
{ problem_number: "2225", problem_title: "합분해", solved_date: "20200205" },
{
problem_number: "2133",
problem_title: "타일 채우기",
solved_date: "20200204",
},
{
problem_number: "11727",
problem_title: "2×n 타일링 2",
solved_date: "20200203",
},
{
problem_number: "11726",
problem_title: "2×n 타일링",
solved_date: "20200203",
},
{
problem_number: "1463",
problem_title: "1로 만들기",
solved_date: "20200203",
},
{
problem_number: "2748",
problem_title: "피보나치 수 2",
solved_date: "20200203",
},
{
problem_number: "2747",
problem_title: "피보나치 수",
solved_date: "20200203",
},
{
problem_number: "11656",
problem_title: "접미사 배열",
solved_date: "20200203",
},
{ problem_number: "10824", problem_title: "네 수", solved_date: "20200203" },
{
problem_number: "2743",
problem_title: "단어 길이 재기",
solved_date: "20200203",
},
{
problem_number: "10820",
problem_title: "문자열 분석",
solved_date: "20200203",
},
{
problem_number: "10808",
problem_title: "알파벳 개수",
solved_date: "20200203",
},
{ problem_number: "11655", problem_title: "ROT13", solved_date: "20200203" },
{
problem_number: "11720",
problem_title: "숫자의 합",
solved_date: "20200131",
},
{ problem_number: "1008", problem_title: "A/B", solved_date: "20200131" },
{
problem_number: "10951",
problem_title: "A+B - 4",
solved_date: "20200131",
},
{
problem_number: "2557",
problem_title: "Hello World",
solved_date: "20200131",
},
{
problem_number: "1021",
problem_title: "회전하는 큐",
solved_date: "20200131",
},
{
problem_number: "1966",
problem_title: "프린터 큐",
solved_date: "20200131",
},
{ problem_number: "2164", problem_title: "카드2", solved_date: "20200131" },
{
problem_number: "10799",
problem_title: "쇠막대기",
solved_date: "20200131",
},
{
problem_number: "17413",
problem_title: "단어 뒤집기 2",
solved_date: "20200131",
},
{ problem_number: "10866", problem_title: "덱", solved_date: "20200131" },
{
problem_number: "1158",
problem_title: "요세푸스 문제",
solved_date: "20200131",
},
{ problem_number: "10845", problem_title: "큐", solved_date: "20200130" },
{ problem_number: "1406", problem_title: "에디터", solved_date: "20200130" },
{
problem_number: "1874",
problem_title: "스택 수열",
solved_date: "20200130",
},
{ problem_number: "9012", problem_title: "괄호", solved_date: "20200130" },
{
problem_number: "9093",
problem_title: "단어 뒤집기",
solved_date: "20200130",
},
{ problem_number: "10828", problem_title: "스택", solved_date: "20200129" },
{
problem_number: "11721",
problem_title: "열 개씩 끊어 출력하기",
solved_date: "20200126",
},
{
problem_number: "11719",
problem_title: "그대로 출력하기 2",
solved_date: "20200126",
},
{
problem_number: "11718",
problem_title: "그대로 출력하기",
solved_date: "20200126",
},
{
problem_number: "10953",
problem_title: "A+B - 6",
solved_date: "20200126",
},
{ problem_number: "2558", problem_title: "A+B - 2", solved_date: "20200126" },
{
problem_number: "10814",
problem_title: "나이순 정렬",
solved_date: "20200123",
},
{
problem_number: "1181",
problem_title: "단어 정렬",
solved_date: "20200122",
},
{
problem_number: "11651",
problem_title: "좌표 정렬하기 2",
solved_date: "20200122",
},
{
problem_number: "11650",
problem_title: "좌표 정렬하기",
solved_date: "20200122",
},
{
problem_number: "1427",
problem_title: "소트인사이드",
solved_date: "20190823",
},
{ problem_number: "2108", problem_title: "통계학", solved_date: "20190823" },
{
problem_number: "10989",
problem_title: "수 정렬하기 3",
solved_date: "20190823",
},
{
problem_number: "2751",
problem_title: "수 정렬하기 2",
solved_date: "20190814",
},
{
problem_number: "2750",
problem_title: "수 정렬하기",
solved_date: "20190814",
},
{
problem_number: "1436",
problem_title: "영화감독 숌",
solved_date: "20190814",
},
{
problem_number: "1018",
problem_title: "체스판 다시 칠하기",
solved_date: "20190814",
},
{ problem_number: "7568", problem_title: "덩치", solved_date: "20190814" },
{ problem_number: "2231", problem_title: "분해합", solved_date: "20190814" },
{ problem_number: "2798", problem_title: "블랙잭", solved_date: "20190814" },
{ problem_number: "1002", problem_title: "터렛", solved_date: "20190814" },
{
problem_number: "3053",
problem_title: "택시 기하학",
solved_date: "20190814",
},
{
problem_number: "4153",
problem_title: "직각삼각형",
solved_date: "20190814",
},
{
problem_number: "3009",
problem_title: "네 번째 점",
solved_date: "20190814",
},
{
problem_number: "1085",
problem_title: "직사각형에서 탈출",
solved_date: "20190814",
},
{
problem_number: "9020",
problem_title: "골드바흐의 추측",
solved_date: "20190814",
},
{
problem_number: "4948",
problem_title: "베르트랑 공준",
solved_date: "20190814",
},
{
problem_number: "1929",
problem_title: "소수 구하기",
solved_date: "20190814",
},
{ problem_number: "2581", problem_title: "소수", solved_date: "20190811" },
{
problem_number: "1978",
problem_title: "소수 찾기",
solved_date: "20190811",
},
{ problem_number: "2292", problem_title: "벌집", solved_date: "20190811" },
{
problem_number: "6064",
problem_title: "카잉 달력",
solved_date: "20190811",
},
{
problem_number: "2775",
problem_title: "부녀회장이 될테야",
solved_date: "20190811",
},
{
problem_number: "10250",
problem_title: "ACM 호텔",
solved_date: "20190811",
},
{
problem_number: "2869",
problem_title: "달팽이는 올라가고 싶다",
solved_date: "20190811",
},
{
problem_number: "1011",
problem_title: "Fly me to the Alpha Centauri",
solved_date: "20190811",
},
{
problem_number: "1193",
problem_title: "분수찾기",
solved_date: "20190810",
},
{
problem_number: "2839",
problem_title: "설탕 배달",
solved_date: "20190809",
},
{
problem_number: "1712",
problem_title: "손익분기점",
solved_date: "20190809",
},
{
problem_number: "1316",
problem_title: "그룹 단어 체커",
solved_date: "20190809",
},
{
problem_number: "2941",
problem_title: "크로아티아 알파벳",
solved_date: "20190809",
},
{ problem_number: "5622", problem_title: "다이얼", solved_date: "20190809" },
{ problem_number: "2908", problem_title: "상수", solved_date: "20190809" },
{
problem_number: "1152",
problem_title: "단어의 개수",
solved_date: "20190809",
},
{
problem_number: "1157",
problem_title: "단어 공부",
solved_date: "20190809",
},
{
problem_number: "2675",
problem_title: "문자열 반복",
solved_date: "20190809",
},
{
problem_number: "10809",
problem_title: "알파벳 찾기",
solved_date: "20190809",
},
{
problem_number: "11720",
problem_title: "숫자의 합",
solved_date: "20190809",
},
{
problem_number: "11654",
problem_title: "아스키 코드",
solved_date: "20190809",
},
{
problem_number: "11729",
problem_title: "하노이 탑 이동 순서",
solved_date: "20190809",
},
{
problem_number: "2447",
problem_title: "별 찍기 - 10",
solved_date: "20190809",
},
{ problem_number: "3052", problem_title: "나머지", solved_date: "20190807" },
{
problem_number: "10818",
problem_title: "최소, 최대",
solved_date: "20190807",
},
{
problem_number: "10872",
problem_title: "팩토리얼",
solved_date: "20190628",
},
{
problem_number: "10870",
problem_title: "피보나치 수 5",
solved_date: "20190628",
},
{ problem_number: "1065", problem_title: "한수", solved_date: "20190628" },
{
problem_number: "4673",
problem_title: "셀프 넘버",
solved_date: "20190628",
},
{
problem_number: "15596",
problem_title: "정수 N개의 합",
solved_date: "20190628",
},
{
problem_number: "4344",
problem_title: "평균은 넘겠지",
solved_date: "20190628",
},
{ problem_number: "2920", problem_title: "음계", solved_date: "20190628" },
{ problem_number: "8958", problem_title: "OX퀴즈", solved_date: "20190628" },
{ problem_number: "1546", problem_title: "평균", solved_date: "20190628" },
{
problem_number: "2577",
problem_title: "숫자의 개수",
solved_date: "20190628",
},
{ problem_number: "2562", problem_title: "최댓값", solved_date: "20190628" },
{
problem_number: "1110",
problem_title: "더하기 사이클",
solved_date: "20190628",
},
{
problem_number: "10951",
problem_title: "A+B - 4",
solved_date: "20190628",
},
{
problem_number: "10952",
problem_title: "A+B - 5",
solved_date: "20190628",
},
{
problem_number: "10871",
problem_title: "X보다 작은 수",
solved_date: "20190628",
},
{
problem_number: "2439",
problem_title: "별 찍기 - 2",
solved_date: "20190628",
},
{
problem_number: "2438",
problem_title: "별 찍기 - 1",
solved_date: "20190628",
},
{
problem_number: "11022",
problem_title: "A+B - 8",
solved_date: "20190628",
},
{
problem_number: "11021",
problem_title: "A+B - 7",
solved_date: "20190628",
},
{ problem_number: "2742", problem_title: "기찍 N", solved_date: "20190628" },
{ problem_number: "2741", problem_title: "N 찍기", solved_date: "20190628" },
{
problem_number: "15552",
problem_title: "빠른 A+B",
solved_date: "20190628",
},
{ problem_number: "8393", problem_title: "합", solved_date: "20190628" },
{
problem_number: "10950",
problem_title: "A+B - 3",
solved_date: "20190628",
},
{ problem_number: "2739", problem_title: "구구단", solved_date: "20190628" },
{ problem_number: "10817", problem_title: "세 수", solved_date: "20190627" },
{
problem_number: "2884",
problem_title: "알람 시계",
solved_date: "20190627",
},
{ problem_number: "2753", problem_title: "윤년", solved_date: "20190627" },
{
problem_number: "9498",
problem_title: "시험 성적",
solved_date: "20190627",
},
{
problem_number: "1330",
problem_title: "두 수 비교하기",
solved_date: "20190627",
},
{ problem_number: "2588", problem_title: "곱셈", solved_date: "20190627" },
{ problem_number: "10430", problem_title: "나머지", solved_date: "20190627" },
{
problem_number: "10869",
problem_title: "사칙연산",
solved_date: "20190627",
},
{ problem_number: "1008", problem_title: "A/B", solved_date: "20190627" },
{ problem_number: "10998", problem_title: "A×B", solved_date: "20190627" },
{ problem_number: "7287", problem_title: "등록", solved_date: "20190627" },
{ problem_number: "10172", problem_title: "개", solved_date: "20190627" },
{ problem_number: "10171", problem_title: "고양이", solved_date: "20190627" },
{
problem_number: "10718",
problem_title: "We love kriii",
solved_date: "20190627",
},
{ problem_number: "1001", problem_title: "A-B", solved_date: "20190607" },
{ problem_number: "1000", problem_title: "A+B", solved_date: "20190607" },
{
problem_number: "2557",
problem_title: "Hello World",
solved_date: "20190607",
},
];
const jwt = require("jsonwebtoken");
const User = require("../models/user");
const jwtMiddleware = async (ctx, next) => {
const token = ctx.cookies.get("access_token");
if (!token) {
return next();
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id: decoded._id,
username: decoded.username,
};
const now = Math.floor(Date.now() / 1000);
if (decoded.exp - now < 60 * 60 * 24 * 3.5) {
const user = await User.findById(decoded._id);
const token = user.generateToken();
ctx.cookies.set("access_token", token, {
maxAge: 1000 * 60 * 60 * 24 * 7, //7days
httpOnly: true,
});
}
return next();
} catch (e) {
return next();
}
};
module.exports = jwtMiddleware;
const mongoose = require("mongoose");
const { Schema } = mongoose;
const ProfileSchema = new Schema({
username: { type: String, required: true, unique: true },
userBJID: String,
solvedBJ: Object,
solvedBJ_date: Object,
friendList: [String],
slackWebHookURL: String,
goalNum: Number,
});
ProfileSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
ProfileSchema.methods.getBJID = function () {
return this.userBJID;
};
ProfileSchema.methods.getBJdata = function () {
return this.solvedBJ;
};
ProfileSchema.methods.getslackURL = function () {
return this.slackWebHookURL;
};
ProfileSchema.methods.getgoalNum = function () {
return this.goalNum;
};
ProfileSchema.methods.getTodaySovled = function () {
if (this.solvedBJ_date) {
return this.solvedBJ_date.presentNum;
}
};
ProfileSchema.methods.serialize = function () {
const data = this.toJSON();
return data;
};
const Profile = mongoose.model("Profile", ProfileSchema);
module.exports = Profile;
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
username: String,
hashedPassword: String,
});
UserSchema.methods.setPassword = async function (password) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result;
};
UserSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
};
UserSchema.methods.serialize = function () {
const data = this.toJSON();
delete data.hashedPassword;
return data;
};
UserSchema.methods.generateToken = function () {
const token = jwt.sign(
{
_id: this.id,
username: this.username,
},
process.env.JWT_SECRET,
{
expiresIn: "7d",
}
);
return token;
};
const User = mongoose.model("User", UserSchema);
module.exports = User;
exports.StringToDate_BJ = function (date_str) {
let arr_date = date_str.split(" "); //yyyy m dd tt MM SS Fomat LIST
let arr_date_r = arr_date.map(function (str) {
let str_r = str.slice(0, -1);
return str_r.length == 1 ? "0" + str_r : str_r;
});
return arr_date_r[0] + arr_date_r[1] + arr_date_r[2]; //YYYYMMDD 형식으로 반환
};
let moment = require("moment");
const problem_set = require("../data/problem_set");
const compareBJ = require("./compareBJ");
exports.analyzeBJ = function (solvedBJ) {
try {
if (solvedBJ) {
let presentDate = moment();
let presentDate_str = presentDate.format("YYYYMMDD");
let latestDate = moment(solvedBJ[0].solved_date, "YYYYMMDD");
let difflatest = presentDate.diff(latestDate, "days");
let latestSolve = solvedBJ[0];
let solvedBJbyDATE = {};
for (let i = 0; i < solvedBJ.length; i++) {
if (!(solvedBJ[i].solved_date in solvedBJbyDATE)) {
solvedBJbyDATE[solvedBJ[i].solved_date] = [];
solvedBJbyDATE[solvedBJ[i].solved_date].push(solvedBJ[i]);
} else {
solvedBJbyDATE[solvedBJ[i].solved_date].push(solvedBJ[i]);
}
}
let latestNum = solvedBJbyDATE[solvedBJ[0].solved_date].length;
let presentNum =
presentDate_str in solvedBJbyDATE
? solvedBJbyDATE[presentDate_str].length
: 0;
let weekNUM = 0;
let monthNUM = 0;
let totalNUM = 0;
for (let i = 0; i < solvedBJ.length; i++) {
let diffDate = presentDate.diff(
moment(solvedBJ[i].solved_date, "YYYYMMDD"),
"days"
);
if (diffDate <= 7) {
weekNUM++;
monthNUM++;
totalNUM++;
} else if (diffDate <= 31) {
monthNUM++;
totalNUM++;
} else {
totalNUM++;
}
}
let unsolved_data = compareBJ.compareBJ(
solvedBJ,
problem_set.problem_set
);
let recommend_data = compareBJ.randomItem(unsolved_data);
let returnOBJ = {
latestDate: latestDate.format("YYYYMMDD"),
difflatest: difflatest,
latestNum: latestNum,
presentNum: presentNum,
weekNum: weekNUM,
monthNum: monthNUM,
totalNum: totalNUM,
solvedBJbyDATE: solvedBJbyDATE,
latestSolve: latestSolve,
recommend_data: recommend_data,
};
return returnOBJ;
}
} catch (e) {
console.log(e);
}
};
exports.compareBJ = function (solvedBJ_new, problem_set) {
try {
let new_obj = [];
for (let i = 0; i < problem_set.length; i++) {
let found = false;
for (let j = 0; j < solvedBJ_new.length; j++) {
if (solvedBJ_new[j].problem_number == problem_set[i].problem_number) {
found = true;
break;
}
}
if (!found) {
new_obj.push(problem_set[i]);
}
}
return new_obj;
} catch (e) {
console.log(e);
}
};
exports.randomItem = function (a) {
return a[Math.floor(Math.random() * a.length)];
};
const axios = require("axios");
const cheerio = require("cheerio");
const StringToDate = require("./StringToDate");
/*
ToDO
- 예외 처리
*/
exports.getBJ = async function (userid) {
let data_list = [];
let next_page_link = "";
await getStartPage(userid).then((html) => {
//시작 페이지를 가져온다.
//같은 객체를 두번 선언한다. 퍼포먼스에 문제 생길수도
//함수에 객체를 넘기는 방법도 있다.
//첫 페이지 가져온다.
data_list.push(getData(html));
next_page_link = getNextPageLink(html);
});
while (next_page_link != -1) {
//다음 페이지를 가져온다.
await getNextPage(next_page_link).then((html) => {
data_list.push(getData(html));
next_page_link = getNextPageLink(html);
});
}
return data_list.flat(1);
};
const getStartPage = async (userid) => {
//유저 아이디 입력
try {
return await axios.get(
"https://www.acmicpc.net/status?user_id=" + userid + "&result_id=4"
);
} catch (error) {
console.log(error);
}
};
const getNextPage = async (link) => {
//링크 입력
try {
return await axios.get(link);
} catch (error) {
console.log(error);
}
};
const getData = (html) => {
//페이지 데이터 파싱
let psArr = [];
const $ = cheerio.load(html.data);
const $bodyList = $("#status-table > tbody");
$bodyList.children().each((index, element) => {
psArr.push({
problem_number: $(element).find("a.problem_title").text(),
problem_title: $(element).find("a.problem_title").attr("title"),
solved_date: StringToDate.StringToDate_BJ(
$(element).find("a.real-time-update").attr("title")
),
});
});
return psArr;
};
const getNextPageLink = (html) => {
//다음 페이지가 있으면 다음 페이지 주소 return, 없으면 -1 return
const $ = cheerio.load(html.data);
return $("#next_page").attr("href")
? "https://www.acmicpc.net/" + $("#next_page").attr("href")
: -1;
};
const Slack = require("slack-node"); // 슬랙 모듈 사용
/*
const webhookUri =
"https://hooks.slack.com/services/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"; // Webhook URL
*/
exports.send = async (message, webhookUri) => {
const slack = new Slack();
slack.setWebhook(webhookUri);
slack.webhook(
{
text: message,
},
function (err, response) {
console.log(response);
}
);
};
var getBJ = require("./getBJ");
var fs = require("fs");
const test = async (userid) => {
let lst = await getBJ.getBJ(userid);
let return_lst = [];
for (let i = 0; i < lst.length; i++) {
return_lst.push(lst[i].problem_number);
}
var stringJson = JSON.stringify(lst) + "\n";
fs.open("test.json", "a", "666", function (err, id) {
if (err) {
console.log("file open err!!");
} else {
fs.write(id, stringJson, null, "utf8", function (err) {
console.log("file was saved!");
});
}
});
};
/*
*/
test("jwseo001");
[
"1517",
"2448",
"1891",
"1074",
"2263",
"1780",
"11728",
"10816",
"10815",
"2109",
"1202",
"1285",
"2138",
"1080",
"11399",
"1931",
"11047",
"15666",
"15665",
"15664",
"15663",
"15657",
"15656",
"15655",
"15654",
"15652",
"15651",
"15650",
"15649",
"6603",
"10971",
"10819",
"10973",
"10974",
"10972",
"7576",
"1248",
"2529",
"15661",
"14501",
"1759",
"14391",
"14889",
"1182",
"11723",
"1748",
"6064",
"1107",
"3085",
"2309",
"1748",
"14500",
"1107",
"1476",
"3085",
"2309",
"1261",
"13549",
"14226",
"13913",
"1697",
"1967",
"1167",
"11725",
"2250",
"1991",
"7562",
"2178",
"4963",
"2667",
"1707",
"11724",
"1260",
"13023",
"11652",
"1377",
"11004",
"10825",
"2751",
"9461",
"1699",
"9095",
"2225",
"2133",
"11727",
"11726",
"1463",
"2748",
"2747",
"11656",
"10824",
"2743",
"10820",
"10808",
"11655",
"11720",
"1008",
"10951",
"2557",
"1021",
"1966",
"2164",
"10799",
"17413",
"10866",
"1158",
"10845",
"1406",
"1874",
"9012",
"9093",
"10828",
"11721",
"11719",
"11718",
"10953",
"2558",
"10814",
"1181",
"11651",
"11650",
"1427",
"2108",
"10989",
"2751",
"2750",
"1436",
"1018",
"7568",
"2231",
"2798",
"1002",
"3053",
"4153",
"3009",
"1085",
"9020",
"4948",
"1929",
"2581",
"1978",
"2292",
"6064",
"2775",
"10250",
"2869",
"1011",
"1193",
"2839",
"1712",
"1316",
"2941",
"5622",
"2908",
"1152",
"1157",
"2675",
"10809",
"11720",
"11654",
"11729",
"2447",
"3052",
"10818",
"10872",
"10870",
"1065",
"4673",
"15596",
"4344",
"2920",
"8958",
"1546",
"2577",
"2562",
"1110",
"10951",
"10952",
"10871",
"2439",
"2438",
"11022",
"11021",
"2742",
"2741",
"15552",
"8393",
"10950",
"2739",
"10817",
"2884",
"2753",
"9498",
"1330",
"2588",
"10430",
"10869",
"1008",
"10998",
"7287",
"10172",
"10171",
"10718",
"1001",
"1000",
"2557"
]
[{"problem_number":"1517","problem_title":"버블 소트","solved_date":"20200621"},{"problem_number":"2448","problem_title":"별 찍기 - 11","solved_date":"20200621"},{"problem_number":"1891","problem_title":"사분면","solved_date":"20200621"},{"problem_number":"1074","problem_title":"Z","solved_date":"20200620"},{"problem_number":"2263","problem_title":"트리의 순회","solved_date":"20200620"},{"problem_number":"1780","problem_title":"종이의 개수","solved_date":"20200619"},{"problem_number":"11728","problem_title":"배열 합치기","solved_date":"20200619"},{"problem_number":"10816","problem_title":"숫자 카드 2","solved_date":"20200619"},{"problem_number":"10815","problem_title":"숫자 카드","solved_date":"20200619"},{"problem_number":"2109","problem_title":"순회강연","solved_date":"20200619"},{"problem_number":"1202","problem_title":"보석 도둑","solved_date":"20200619"},{"problem_number":"1285","problem_title":"동전 뒤집기","solved_date":"20200617"},{"problem_number":"2138","problem_title":"전구와 스위치","solved_date":"20200617"},{"problem_number":"1080","problem_title":"행렬","solved_date":"20200617"},{"problem_number":"11399","problem_title":"ATM","solved_date":"20200616"},{"problem_number":"1931","problem_title":"회의실배정","solved_date":"20200616"},{"problem_number":"11047","problem_title":"동전 0","solved_date":"20200615"},{"problem_number":"15666","problem_title":"N과 M (12)","solved_date":"20200614"},{"problem_number":"15665","problem_title":"N과 M (11)","solved_date":"20200614"},{"problem_number":"15664","problem_title":"N과 M (10)","solved_date":"20200614"},{"problem_number":"15663","problem_title":"N과 M (9)","solved_date":"20200614"},{"problem_number":"15657","problem_title":"N과 M (8)","solved_date":"20200614"},{"problem_number":"15656","problem_title":"N과 M (7)","solved_date":"20200614"},{"problem_number":"15655","problem_title":"N과 M (6)","solved_date":"20200614"},{"problem_number":"15654","problem_title":"N과 M (5)","solved_date":"20200614"},{"problem_number":"15652","problem_title":"N과 M (4)","solved_date":"20200614"},{"problem_number":"15651","problem_title":"N과 M (3)","solved_date":"20200612"},{"problem_number":"15650","problem_title":"N과 M (2)","solved_date":"20200612"},{"problem_number":"15649","problem_title":"N과 M (1)","solved_date":"20200612"},{"problem_number":"6603","problem_title":"로또","solved_date":"20200612"},{"problem_number":"10971","problem_title":"외판원 순회 2","solved_date":"20200611"},{"problem_number":"10819","problem_title":"차이를 최대로","solved_date":"20200611"},{"problem_number":"10973","problem_title":"이전 순열","solved_date":"20200611"},{"problem_number":"10974","problem_title":"모든 순열","solved_date":"20200611"},{"problem_number":"10972","problem_title":"다음 순열","solved_date":"20200611"},{"problem_number":"7576","problem_title":"토마토","solved_date":"20200608"},{"problem_number":"1248","problem_title":"맞춰봐","solved_date":"20200312"},{"problem_number":"2529","problem_title":"부등호","solved_date":"20200311"},{"problem_number":"15661","problem_title":"링크와 스타트","solved_date":"20200311"},{"problem_number":"14501","problem_title":"퇴사","solved_date":"20200311"},{"problem_number":"1759","problem_title":"암호 만들기","solved_date":"20200310"},{"problem_number":"14391","problem_title":"종이 조각","solved_date":"20200306"},{"problem_number":"14889","problem_title":"스타트와 링크","solved_date":"20200305"},{"problem_number":"1182","problem_title":"부분수열의 합","solved_date":"20200305"},{"problem_number":"11723","problem_title":"집합","solved_date":"20200305"},{"problem_number":"1748","problem_title":"수 이어 쓰기 1","solved_date":"20200305"},{"problem_number":"6064","problem_title":"카잉 달력","solved_date":"20200305"},{"problem_number":"1107","problem_title":"리모컨","solved_date":"20200305"},{"problem_number":"3085","problem_title":"사탕 게임","solved_date":"20200305"},{"problem_number":"2309","problem_title":"일곱 난쟁이","solved_date":"20200305"},{"problem_number":"1748","problem_title":"수 이어 쓰기 1","solved_date":"20200228"},{"problem_number":"14500","problem_title":"테트로미노","solved_date":"20200228"},{"problem_number":"1107","problem_title":"리모컨","solved_date":"20200228"},{"problem_number":"1476","problem_title":"날짜 계산","solved_date":"20200228"},{"problem_number":"3085","problem_title":"사탕 게임","solved_date":"20200228"},{"problem_number":"2309","problem_title":"일곱 난쟁이","solved_date":"20200228"},{"problem_number":"1261","problem_title":"알고스팟","solved_date":"20200228"},{"problem_number":"13549","problem_title":"숨바꼭질 3","solved_date":"20200227"},{"problem_number":"14226","problem_title":"이모티콘","solved_date":"20200227"},{"problem_number":"13913","problem_title":"숨바꼭질 4","solved_date":"20200227"},{"problem_number":"1697","problem_title":"숨바꼭질","solved_date":"20200227"},{"problem_number":"1967","problem_title":"트리의 지름","solved_date":"20200221"},{"problem_number":"1167","problem_title":"트리의 지름","solved_date":"20200221"},{"problem_number":"11725","problem_title":"트리의 부모 찾기","solved_date":"20200221"},{"problem_number":"2250","problem_title":"트리의 높이와 너비","solved_date":"20200221"},{"problem_number":"1991","problem_title":"트리 순회","solved_date":"20200221"},{"problem_number":"7562","problem_title":"나이트의 이동","solved_date":"20200220"},{"problem_number":"2178","problem_title":"미로 탐색","solved_date":"20200220"},{"problem_number":"4963","problem_title":"섬의 개수","solved_date":"20200219"},{"problem_number":"2667","problem_title":"단지번호붙이기","solved_date":"20200219"},{"problem_number":"1707","problem_title":"이분 그래프","solved_date":"20200217"},{"problem_number":"11724","problem_title":"연결 요소의 개수","solved_date":"20200214"},{"problem_number":"1260","problem_title":"DFS와 BFS","solved_date":"20200214"},{"problem_number":"13023","problem_title":"ABCDE","solved_date":"20200213"},{"problem_number":"11652","problem_title":"카드","solved_date":"20200210"},{"problem_number":"1377","problem_title":"버블 소트","solved_date":"20200210"},{"problem_number":"11004","problem_title":"K번째 수","solved_date":"20200210"},{"problem_number":"10825","problem_title":"국영수","solved_date":"20200210"},{"problem_number":"2751","problem_title":"수 정렬하기 2","solved_date":"20200210"},{"problem_number":"9461","problem_title":"파도반 수열","solved_date":"20200205"},{"problem_number":"1699","problem_title":"제곱수의 합","solved_date":"20200205"},{"problem_number":"9095","problem_title":"1, 2, 3 더하기","solved_date":"20200205"},{"problem_number":"2225","problem_title":"합분해","solved_date":"20200205"},{"problem_number":"2133","problem_title":"타일 채우기","solved_date":"20200204"},{"problem_number":"11727","problem_title":"2×n 타일링 2","solved_date":"20200203"},{"problem_number":"11726","problem_title":"2×n 타일링","solved_date":"20200203"},{"problem_number":"1463","problem_title":"1로 만들기","solved_date":"20200203"},{"problem_number":"2748","problem_title":"피보나치 수 2","solved_date":"20200203"},{"problem_number":"2747","problem_title":"피보나치 수","solved_date":"20200203"},{"problem_number":"11656","problem_title":"접미사 배열","solved_date":"20200203"},{"problem_number":"10824","problem_title":"네 수","solved_date":"20200203"},{"problem_number":"2743","problem_title":"단어 길이 재기","solved_date":"20200203"},{"problem_number":"10820","problem_title":"문자열 분석","solved_date":"20200203"},{"problem_number":"10808","problem_title":"알파벳 개수","solved_date":"20200203"},{"problem_number":"11655","problem_title":"ROT13","solved_date":"20200203"},{"problem_number":"11720","problem_title":"숫자의 합","solved_date":"20200131"},{"problem_number":"1008","problem_title":"A/B","solved_date":"20200131"},{"problem_number":"10951","problem_title":"A+B - 4","solved_date":"20200131"},{"problem_number":"2557","problem_title":"Hello World","solved_date":"20200131"},{"problem_number":"1021","problem_title":"회전하는 큐","solved_date":"20200131"},{"problem_number":"1966","problem_title":"프린터 큐","solved_date":"20200131"},{"problem_number":"2164","problem_title":"카드2","solved_date":"20200131"},{"problem_number":"10799","problem_title":"쇠막대기","solved_date":"20200131"},{"problem_number":"17413","problem_title":"단어 뒤집기 2","solved_date":"20200131"},{"problem_number":"10866","problem_title":"덱","solved_date":"20200131"},{"problem_number":"1158","problem_title":"요세푸스 문제","solved_date":"20200131"},{"problem_number":"10845","problem_title":"큐","solved_date":"20200130"},{"problem_number":"1406","problem_title":"에디터","solved_date":"20200130"},{"problem_number":"1874","problem_title":"스택 수열","solved_date":"20200130"},{"problem_number":"9012","problem_title":"괄호","solved_date":"20200130"},{"problem_number":"9093","problem_title":"단어 뒤집기","solved_date":"20200130"},{"problem_number":"10828","problem_title":"스택","solved_date":"20200129"},{"problem_number":"11721","problem_title":"열 개씩 끊어 출력하기","solved_date":"20200126"},{"problem_number":"11719","problem_title":"그대로 출력하기 2","solved_date":"20200126"},{"problem_number":"11718","problem_title":"그대로 출력하기","solved_date":"20200126"},{"problem_number":"10953","problem_title":"A+B - 6","solved_date":"20200126"},{"problem_number":"2558","problem_title":"A+B - 2","solved_date":"20200126"},{"problem_number":"10814","problem_title":"나이순 정렬","solved_date":"20200123"},{"problem_number":"1181","problem_title":"단어 정렬","solved_date":"20200122"},{"problem_number":"11651","problem_title":"좌표 정렬하기 2","solved_date":"20200122"},{"problem_number":"11650","problem_title":"좌표 정렬하기","solved_date":"20200122"},{"problem_number":"1427","problem_title":"소트인사이드","solved_date":"20190823"},{"problem_number":"2108","problem_title":"통계학","solved_date":"20190823"},{"problem_number":"10989","problem_title":"수 정렬하기 3","solved_date":"20190823"},{"problem_number":"2751","problem_title":"수 정렬하기 2","solved_date":"20190814"},{"problem_number":"2750","problem_title":"수 정렬하기","solved_date":"20190814"},{"problem_number":"1436","problem_title":"영화감독 숌","solved_date":"20190814"},{"problem_number":"1018","problem_title":"체스판 다시 칠하기","solved_date":"20190814"},{"problem_number":"7568","problem_title":"덩치","solved_date":"20190814"},{"problem_number":"2231","problem_title":"분해합","solved_date":"20190814"},{"problem_number":"2798","problem_title":"블랙잭","solved_date":"20190814"},{"problem_number":"1002","problem_title":"터렛","solved_date":"20190814"},{"problem_number":"3053","problem_title":"택시 기하학","solved_date":"20190814"},{"problem_number":"4153","problem_title":"직각삼각형","solved_date":"20190814"},{"problem_number":"3009","problem_title":"네 번째 점","solved_date":"20190814"},{"problem_number":"1085","problem_title":"직사각형에서 탈출","solved_date":"20190814"},{"problem_number":"9020","problem_title":"골드바흐의 추측","solved_date":"20190814"},{"problem_number":"4948","problem_title":"베르트랑 공준","solved_date":"20190814"},{"problem_number":"1929","problem_title":"소수 구하기","solved_date":"20190814"},{"problem_number":"2581","problem_title":"소수","solved_date":"20190811"},{"problem_number":"1978","problem_title":"소수 찾기","solved_date":"20190811"},{"problem_number":"2292","problem_title":"벌집","solved_date":"20190811"},{"problem_number":"6064","problem_title":"카잉 달력","solved_date":"20190811"},{"problem_number":"2775","problem_title":"부녀회장이 될테야","solved_date":"20190811"},{"problem_number":"10250","problem_title":"ACM 호텔","solved_date":"20190811"},{"problem_number":"2869","problem_title":"달팽이는 올라가고 싶다","solved_date":"20190811"},{"problem_number":"1011","problem_title":"Fly me to the Alpha Centauri","solved_date":"20190811"},{"problem_number":"1193","problem_title":"분수찾기","solved_date":"20190810"},{"problem_number":"2839","problem_title":"설탕 배달","solved_date":"20190809"},{"problem_number":"1712","problem_title":"손익분기점","solved_date":"20190809"},{"problem_number":"1316","problem_title":"그룹 단어 체커","solved_date":"20190809"},{"problem_number":"2941","problem_title":"크로아티아 알파벳","solved_date":"20190809"},{"problem_number":"5622","problem_title":"다이얼","solved_date":"20190809"},{"problem_number":"2908","problem_title":"상수","solved_date":"20190809"},{"problem_number":"1152","problem_title":"단어의 개수","solved_date":"20190809"},{"problem_number":"1157","problem_title":"단어 공부","solved_date":"20190809"},{"problem_number":"2675","problem_title":"문자열 반복","solved_date":"20190809"},{"problem_number":"10809","problem_title":"알파벳 찾기","solved_date":"20190809"},{"problem_number":"11720","problem_title":"숫자의 합","solved_date":"20190809"},{"problem_number":"11654","problem_title":"아스키 코드","solved_date":"20190809"},{"problem_number":"11729","problem_title":"하노이 탑 이동 순서","solved_date":"20190809"},{"problem_number":"2447","problem_title":"별 찍기 - 10","solved_date":"20190809"},{"problem_number":"3052","problem_title":"나머지","solved_date":"20190807"},{"problem_number":"10818","problem_title":"최소, 최대","solved_date":"20190807"},{"problem_number":"10872","problem_title":"팩토리얼","solved_date":"20190628"},{"problem_number":"10870","problem_title":"피보나치 수 5","solved_date":"20190628"},{"problem_number":"1065","problem_title":"한수","solved_date":"20190628"},{"problem_number":"4673","problem_title":"셀프 넘버","solved_date":"20190628"},{"problem_number":"15596","problem_title":"정수 N개의 합","solved_date":"20190628"},{"problem_number":"4344","problem_title":"평균은 넘겠지","solved_date":"20190628"},{"problem_number":"2920","problem_title":"음계","solved_date":"20190628"},{"problem_number":"8958","problem_title":"OX퀴즈","solved_date":"20190628"},{"problem_number":"1546","problem_title":"평균","solved_date":"20190628"},{"problem_number":"2577","problem_title":"숫자의 개수","solved_date":"20190628"},{"problem_number":"2562","problem_title":"최댓값","solved_date":"20190628"},{"problem_number":"1110","problem_title":"더하기 사이클","solved_date":"20190628"},{"problem_number":"10951","problem_title":"A+B - 4","solved_date":"20190628"},{"problem_number":"10952","problem_title":"A+B - 5","solved_date":"20190628"},{"problem_number":"10871","problem_title":"X보다 작은 수","solved_date":"20190628"},{"problem_number":"2439","problem_title":"별 찍기 - 2","solved_date":"20190628"},{"problem_number":"2438","problem_title":"별 찍기 - 1","solved_date":"20190628"},{"problem_number":"11022","problem_title":"A+B - 8","solved_date":"20190628"},{"problem_number":"11021","problem_title":"A+B - 7","solved_date":"20190628"},{"problem_number":"2742","problem_title":"기찍 N","solved_date":"20190628"},{"problem_number":"2741","problem_title":"N 찍기","solved_date":"20190628"},{"problem_number":"15552","problem_title":"빠른 A+B","solved_date":"20190628"},{"problem_number":"8393","problem_title":"합","solved_date":"20190628"},{"problem_number":"10950","problem_title":"A+B - 3","solved_date":"20190628"},{"problem_number":"2739","problem_title":"구구단","solved_date":"20190628"},{"problem_number":"10817","problem_title":"세 수","solved_date":"20190627"},{"problem_number":"2884","problem_title":"알람 시계","solved_date":"20190627"},{"problem_number":"2753","problem_title":"윤년","solved_date":"20190627"},{"problem_number":"9498","problem_title":"시험 성적","solved_date":"20190627"},{"problem_number":"1330","problem_title":"두 수 비교하기","solved_date":"20190627"},{"problem_number":"2588","problem_title":"곱셈","solved_date":"20190627"},{"problem_number":"10430","problem_title":"나머지","solved_date":"20190627"},{"problem_number":"10869","problem_title":"사칙연산","solved_date":"20190627"},{"problem_number":"1008","problem_title":"A/B","solved_date":"20190627"},{"problem_number":"10998","problem_title":"A×B","solved_date":"20190627"},{"problem_number":"7287","problem_title":"등록","solved_date":"20190627"},{"problem_number":"10172","problem_title":"개","solved_date":"20190627"},{"problem_number":"10171","problem_title":"고양이","solved_date":"20190627"},{"problem_number":"10718","problem_title":"We love kriii","solved_date":"20190627"},{"problem_number":"1001","problem_title":"A-B","solved_date":"20190607"},{"problem_number":"1000","problem_title":"A+B","solved_date":"20190607"},{"problem_number":"2557","problem_title":"Hello World","solved_date":"20190607"}]
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.