Showing
53 changed files
with
2179 additions
and
57 deletions
jaksimsamil-page/package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
... | @@ -3,13 +3,25 @@ | ... | @@ -3,13 +3,25 @@ |
3 | "version": "0.1.0", | 3 | "version": "0.1.0", |
4 | "private": true, | 4 | "private": true, |
5 | "dependencies": { | 5 | "dependencies": { |
6 | + "@material-ui/core": "^4.10.2", | ||
6 | "@testing-library/jest-dom": "^4.2.4", | 7 | "@testing-library/jest-dom": "^4.2.4", |
7 | "@testing-library/react": "^9.3.2", | 8 | "@testing-library/react": "^9.3.2", |
8 | "@testing-library/user-event": "^7.1.2", | 9 | "@testing-library/user-event": "^7.1.2", |
10 | + "axios": "^0.19.2", | ||
11 | + "immer": "^7.0.5", | ||
12 | + "include-media": "^1.4.9", | ||
13 | + "moment": "^2.27.0", | ||
14 | + "open-color": "^1.7.0", | ||
9 | "react": "^16.13.1", | 15 | "react": "^16.13.1", |
10 | "react-dom": "^16.13.1", | 16 | "react-dom": "^16.13.1", |
17 | + "react-redux": "^7.2.0", | ||
11 | "react-router-dom": "^5.2.0", | 18 | "react-router-dom": "^5.2.0", |
12 | - "react-scripts": "3.4.1" | 19 | + "react-scripts": "3.4.1", |
20 | + "redux": "^4.0.5", | ||
21 | + "redux-actions": "^2.6.5", | ||
22 | + "redux-devtools-extension": "^2.13.8", | ||
23 | + "redux-saga": "^1.1.3", | ||
24 | + "styled-components": "^5.1.1" | ||
13 | }, | 25 | }, |
14 | "scripts": { | 26 | "scripts": { |
15 | "start": "react-scripts start", | 27 | "start": "react-scripts start", |
... | @@ -31,5 +43,6 @@ | ... | @@ -31,5 +43,6 @@ |
31 | "last 1 firefox version", | 43 | "last 1 firefox version", |
32 | "last 1 safari version" | 44 | "last 1 safari version" |
33 | ] | 45 | ] |
34 | - } | 46 | + }, |
47 | + "proxy": "http://localhost:4000" | ||
35 | } | 48 | } | ... | ... |
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | -import logo from './logo.svg'; | 2 | +import { Route } from 'react-router-dom'; |
3 | import './App.css'; | 3 | import './App.css'; |
4 | +import LoginPage from './pages/LoginPage'; | ||
5 | +import RegisterPage from './pages/RegisterPage'; | ||
6 | +import HomePage from './pages/HomePage'; | ||
7 | +import SettingPage from './pages/SettingPage'; | ||
4 | 8 | ||
5 | function App() { | 9 | function App() { |
6 | return ( | 10 | return ( |
7 | - <div className="App"> | 11 | + <> |
8 | - <header className="App-header"> | 12 | + <Route component={HomePage} path={['/@:username', '/']} exact /> |
9 | - <img src={logo} className="App-logo" alt="logo" /> | 13 | + <Route component={LoginPage} path="/login" /> |
10 | - <p> | 14 | + <Route component={RegisterPage} path="/register" /> |
11 | - Edit <code>src/App.js</code> and save to reload. | 15 | + <Route component={SettingPage} path="/setting" /> |
12 | - </p> | 16 | + </> |
13 | - <a | ||
14 | - className="App-link" | ||
15 | - href="https://reactjs.org" | ||
16 | - target="_blank" | ||
17 | - rel="noopener noreferrer" | ||
18 | - > | ||
19 | - Learn React | ||
20 | - </a> | ||
21 | - </header> | ||
22 | - </div> | ||
23 | ); | 17 | ); |
24 | } | 18 | } |
25 | 19 | ... | ... |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import { Link } from 'react-router-dom'; | ||
4 | +import palette from '../../lib/styles/palette'; | ||
5 | +import Button from '../common/Button'; | ||
6 | + | ||
7 | +const AuthFormBlock = styled.div` | ||
8 | + h3 { | ||
9 | + margin: 0; | ||
10 | + color: ${palette.gray[8]}; | ||
11 | + margin-bottom: 1rem; | ||
12 | + } | ||
13 | +`; | ||
14 | + | ||
15 | +const StyledInput = styled.input` | ||
16 | + font-size: 1rem; | ||
17 | + border: none; | ||
18 | + border-bottom: 1px solid ${palette.gray[5]}; | ||
19 | + padding-bottom: 0.5rem; | ||
20 | + outline: none; | ||
21 | + width: 100%; | ||
22 | + &:focus { | ||
23 | + color: $oc-teal-7; | ||
24 | + border-bottom: 1px solid ${palette.gray[7]}; | ||
25 | + } | ||
26 | + & + & { | ||
27 | + margin-top: 1rem; | ||
28 | + } | ||
29 | +`; | ||
30 | + | ||
31 | +const Footer = styled.div` | ||
32 | + margin-top: 2rem; | ||
33 | + text-align: right; | ||
34 | + a { | ||
35 | + color: ${palette.gray[6]}; | ||
36 | + text-decoration: underline; | ||
37 | + &:hover { | ||
38 | + color: ${palette.gray[9]}; | ||
39 | + } | ||
40 | + } | ||
41 | +`; | ||
42 | + | ||
43 | +const ButtonWithMarginTop = styled(Button)` | ||
44 | + margin-top: 1rem; | ||
45 | +`; | ||
46 | + | ||
47 | +const ErrorMessage = styled.div` | ||
48 | + color: red; | ||
49 | + text-align: center; | ||
50 | + font-size: 0.875rem; | ||
51 | + margin-top: 1rem; | ||
52 | +`; | ||
53 | + | ||
54 | +const textMap = { | ||
55 | + login: '로그인', | ||
56 | + register: '회원가입', | ||
57 | +}; | ||
58 | + | ||
59 | +const AuthForm = ({ type, form, onChange, onSubmit, error }) => { | ||
60 | + const text = textMap[type]; | ||
61 | + return ( | ||
62 | + <AuthFormBlock> | ||
63 | + <h3>{text}</h3> | ||
64 | + <form onSubmit={onSubmit}> | ||
65 | + <StyledInput | ||
66 | + autoComplete="username" | ||
67 | + name="username" | ||
68 | + placeholder="아이디" | ||
69 | + onChange={onChange} | ||
70 | + value={form.username} | ||
71 | + /> | ||
72 | + <StyledInput | ||
73 | + autoComplete="new-password" | ||
74 | + name="password" | ||
75 | + placeholder="비밀번호" | ||
76 | + type="password" | ||
77 | + onChange={onChange} | ||
78 | + value={form.password} | ||
79 | + /> | ||
80 | + {type === 'register' && ( | ||
81 | + <StyledInput | ||
82 | + autoComplete="new-password" | ||
83 | + name="passwordConfirm" | ||
84 | + placeholder="비밀번호 확인" | ||
85 | + type="password" | ||
86 | + onChange={onChange} | ||
87 | + value={form.passwordConfirm} | ||
88 | + /> | ||
89 | + )} | ||
90 | + {error && <ErrorMessage>{error}</ErrorMessage>} | ||
91 | + <ButtonWithMarginTop cyan fullWidth> | ||
92 | + {text} | ||
93 | + </ButtonWithMarginTop> | ||
94 | + </form> | ||
95 | + <Footer> | ||
96 | + {type === 'login' ? ( | ||
97 | + <Link to="/register">회원가입</Link> | ||
98 | + ) : ( | ||
99 | + <Link to="/login">로그인</Link> | ||
100 | + )} | ||
101 | + </Footer> | ||
102 | + </AuthFormBlock> | ||
103 | + ); | ||
104 | +}; | ||
105 | + | ||
106 | +export default AuthForm; |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import palette from '../../lib/styles/palette'; | ||
4 | +import { Link } from 'react-router-dom'; | ||
5 | +/* | ||
6 | +register/login Layout | ||
7 | +*/ | ||
8 | +const AuthTemplateBlock = styled.div` | ||
9 | + position: absolute; | ||
10 | + left: 0; | ||
11 | + top: 0; | ||
12 | + bottom: 0; | ||
13 | + right: 0; | ||
14 | + background: ${palette.gray[2]}; | ||
15 | + display: flex; | ||
16 | + flex-direction: column; | ||
17 | + justify-content: center; | ||
18 | + align-items: center; | ||
19 | +`; | ||
20 | + | ||
21 | +const WhiteBox = styled.div` | ||
22 | + .logo-area { | ||
23 | + display: block; | ||
24 | + padding-bottom: 2rem; | ||
25 | + text-align: center; | ||
26 | + font-weight: bold; | ||
27 | + letter-spacing: 2px; | ||
28 | + } | ||
29 | + box-shadow: 0 0 8px rgba(0, 0, 0, 0.025); | ||
30 | + padding: 2rem; | ||
31 | + width: 360px; | ||
32 | + background: white; | ||
33 | + border-radius: 2px; | ||
34 | +`; | ||
35 | + | ||
36 | +const AuthTemplate = ({ children }) => { | ||
37 | + return ( | ||
38 | + <AuthTemplateBlock> | ||
39 | + <WhiteBox> | ||
40 | + <div className="logo-area"> | ||
41 | + <Link to="/">작심삼일</Link> | ||
42 | + </div> | ||
43 | + {children} | ||
44 | + </WhiteBox> | ||
45 | + </AuthTemplateBlock> | ||
46 | + ); | ||
47 | +}; | ||
48 | + | ||
49 | +export default AuthTemplate; |
1 | +import React from 'react'; | ||
2 | +import styled, { css } from 'styled-components'; | ||
3 | +import palette from '../../lib/styles/palette'; | ||
4 | +import { withRouter } from 'react-router-dom'; | ||
5 | + | ||
6 | +const StyledButton = styled.button` | ||
7 | + border: none; | ||
8 | + border-radius: 4px; | ||
9 | + font-size: 1rem; | ||
10 | + font-weight: bold; | ||
11 | + padding: 0.25rem 1rem; | ||
12 | + color: white; | ||
13 | + outline: none; | ||
14 | + cursor: pointer; | ||
15 | + | ||
16 | + background: ${palette.gray[8]}; | ||
17 | + &:hover { | ||
18 | + background: ${palette.gray[6]}; | ||
19 | + } | ||
20 | + ${props => | ||
21 | + props.fullWidth && | ||
22 | + css` | ||
23 | + padding-top: 0.75rem; | ||
24 | + padding-bottom: 0.75rem; | ||
25 | + width: 100%; | ||
26 | + font-size: 1.125rem; | ||
27 | + `} | ||
28 | + | ||
29 | + ${props => | ||
30 | + props.cyan && | ||
31 | + css` | ||
32 | + background: ${palette.cyan[5]}; | ||
33 | + &:hover { | ||
34 | + background: ${palette.cyan[4]}; | ||
35 | + } | ||
36 | + `} | ||
37 | +`; | ||
38 | + | ||
39 | +const Button = ({ to, history, ...rest }) => { | ||
40 | + const onClick = e => { | ||
41 | + if (to) { | ||
42 | + history.push(to); | ||
43 | + } | ||
44 | + if (rest.onClick) { | ||
45 | + rest.onClick(e); | ||
46 | + } | ||
47 | + }; | ||
48 | + return <StyledButton {...rest} onClick={onClick} />; | ||
49 | +}; | ||
50 | +export default withRouter(Button); |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import { NavLink } from 'react-router-dom'; | ||
4 | + | ||
5 | +const categories = [ | ||
6 | + { | ||
7 | + name: 'home', | ||
8 | + text: '홈', | ||
9 | + }, | ||
10 | + { | ||
11 | + name: 'setting', | ||
12 | + text: '설정', | ||
13 | + }, | ||
14 | +]; | ||
15 | + | ||
16 | +const CategoriesBlock = styled.div` | ||
17 | + display: flex; | ||
18 | + padding: 1rem; | ||
19 | + margin: 0 auto; | ||
20 | + @media screen and (max-width: 768px) { | ||
21 | + width: 100%; | ||
22 | + overflow-x: auto; | ||
23 | + } | ||
24 | +`; | ||
25 | + | ||
26 | +const Category = styled(NavLink)` | ||
27 | + font-size: 1.2rem; | ||
28 | + cursor: pointer; | ||
29 | + white-space: pre; | ||
30 | + text-decoration: none; | ||
31 | + color: inherit; | ||
32 | + padding-bottom: 0.25rem; | ||
33 | + &:hover { | ||
34 | + color: #495057; | ||
35 | + } | ||
36 | + & + & { | ||
37 | + margin-left: 2rem; | ||
38 | + } | ||
39 | + &.active { | ||
40 | + font-weight: 600; | ||
41 | + border-bottom: 2px solid #22b8cf; | ||
42 | + color: #22b8cf; | ||
43 | + &:hover { | ||
44 | + color: #3bc9db; | ||
45 | + } | ||
46 | + } | ||
47 | +`; | ||
48 | + | ||
49 | +const Categories = () => { | ||
50 | + return ( | ||
51 | + <CategoriesBlock> | ||
52 | + {categories.map((c) => ( | ||
53 | + <Category | ||
54 | + activeClassName="active" | ||
55 | + key={c.name} | ||
56 | + exact={c.name === 'home'} | ||
57 | + to={c.name === 'home' ? '/' : `/${c.name}`} | ||
58 | + > | ||
59 | + {c.text} | ||
60 | + </Category> | ||
61 | + ))} | ||
62 | + </CategoriesBlock> | ||
63 | + ); | ||
64 | +}; | ||
65 | + | ||
66 | +export default Categories; |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import Responsive from './Responsive'; | ||
4 | +import Button from './Button'; | ||
5 | +import { Link } from 'react-router-dom'; | ||
6 | +import Categories from './Categories'; | ||
7 | + | ||
8 | +const HeaderBlock = styled.div` | ||
9 | + position: fixed; | ||
10 | + width: 100%; | ||
11 | + background: white; | ||
12 | + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); | ||
13 | +`; | ||
14 | + | ||
15 | +const Wrapper = styled(Responsive)` | ||
16 | + height: 4rem; | ||
17 | + display: flex; | ||
18 | + align-items: center; | ||
19 | + justify-content: space-between; | ||
20 | + .logo { | ||
21 | + font-size: 1.125rem; | ||
22 | + font-weight: 800; | ||
23 | + letter-spacing: 2px; | ||
24 | + } | ||
25 | + .right { | ||
26 | + display: flex; | ||
27 | + align-items: center; | ||
28 | + } | ||
29 | +`; | ||
30 | + | ||
31 | +const Spacer = styled.div` | ||
32 | + height: 4rem; | ||
33 | +`; | ||
34 | +const UserInfo = styled.div` | ||
35 | + font-weight: 800; | ||
36 | + margin-right: 1rem; | ||
37 | +`; | ||
38 | + | ||
39 | +const Header = ({ user, onLogout, category, onSelect }) => { | ||
40 | + return ( | ||
41 | + <> | ||
42 | + <HeaderBlock> | ||
43 | + <Wrapper> | ||
44 | + <Link to="/" className="logo"> | ||
45 | + 작심삼일 | ||
46 | + </Link> | ||
47 | + <Categories | ||
48 | + category={category} | ||
49 | + onSelect={onSelect} | ||
50 | + className="right" | ||
51 | + /> | ||
52 | + {user ? ( | ||
53 | + <div className="right"> | ||
54 | + <UserInfo>{user.username}</UserInfo> | ||
55 | + <Button onClick={onLogout}>로그아웃</Button> | ||
56 | + </div> | ||
57 | + ) : ( | ||
58 | + <div className="right"> | ||
59 | + <Button to="/login">로그인</Button> | ||
60 | + </div> | ||
61 | + )} | ||
62 | + </Wrapper> | ||
63 | + </HeaderBlock> | ||
64 | + <Spacer /> | ||
65 | + </> | ||
66 | + ); | ||
67 | +}; | ||
68 | + | ||
69 | +export default Header; |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | + | ||
4 | +const ResponsiveBlock = styled.div` | ||
5 | + padding-left: 1rem; | ||
6 | + padding-right: 1rem; | ||
7 | + width: 1024px; | ||
8 | + margin: 0 auto; | ||
9 | + | ||
10 | + @media (max-width: 1024px) { | ||
11 | + width: 768px; | ||
12 | + } | ||
13 | + @media (max-width: 768px) { | ||
14 | + width: 100%; | ||
15 | + } | ||
16 | +`; | ||
17 | + | ||
18 | +const Responsive = ({ children, ...rest }) => { | ||
19 | + return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>; | ||
20 | +}; | ||
21 | + | ||
22 | +export default Responsive; |
1 | +import React from 'react'; | ||
2 | +import { makeStyles } from '@material-ui/core/styles'; | ||
3 | +import Paper from '@material-ui/core/Paper'; | ||
4 | +import Grid from '@material-ui/core/Grid'; | ||
5 | +import palette from '../../lib/styles/palette'; | ||
6 | +const useStyles = makeStyles((theme) => ({ | ||
7 | + root: { | ||
8 | + flexGrow: 1, | ||
9 | + background: palette.gray[2], | ||
10 | + }, | ||
11 | + paper: { | ||
12 | + padding: theme.spacing(2), | ||
13 | + textAlign: 'center', | ||
14 | + color: theme.palette.text.secondary, | ||
15 | + }, | ||
16 | +})); | ||
17 | +const HomeForm = () => { | ||
18 | + const classes = useStyles(); | ||
19 | + return ( | ||
20 | + <div className={classes.root}> | ||
21 | + <Grid container spacing={3}> | ||
22 | + <Grid item xs={12}> | ||
23 | + <Paper className={classes.paper}>xs=12</Paper> | ||
24 | + </Grid> | ||
25 | + <Grid item xs={6}> | ||
26 | + <Paper className={classes.paper}>xs=6</Paper> | ||
27 | + </Grid> | ||
28 | + <Grid item xs={6}> | ||
29 | + <Paper className={classes.paper}>xs=6</Paper> | ||
30 | + </Grid> | ||
31 | + <Grid item xs={3}> | ||
32 | + <Paper className={classes.paper}>xs=3</Paper> | ||
33 | + </Grid> | ||
34 | + <Grid item xs={3}> | ||
35 | + <Paper className={classes.paper}>xs=3</Paper> | ||
36 | + </Grid> | ||
37 | + <Grid item xs={3}> | ||
38 | + <Paper className={classes.paper}>xs=3</Paper> | ||
39 | + </Grid> | ||
40 | + <Grid item xs={3}> | ||
41 | + <Paper className={classes.paper}>xs=3</Paper> | ||
42 | + </Grid> | ||
43 | + </Grid> | ||
44 | + </div> | ||
45 | + ); | ||
46 | +}; | ||
47 | + | ||
48 | +export default HomeForm; |
1 | +import React from 'react'; | ||
2 | +import { makeStyles } from '@material-ui/core/styles'; | ||
3 | +import Button from '@material-ui/core/Button'; | ||
4 | +import TextField from '@material-ui/core/TextField'; | ||
5 | + | ||
6 | +const useStyles = makeStyles((theme) => ({ | ||
7 | + root: { | ||
8 | + '& > *': { | ||
9 | + margin: theme.spacing(1), | ||
10 | + }, | ||
11 | + }, | ||
12 | +})); | ||
13 | + | ||
14 | +const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => { | ||
15 | + const classes = useStyles(); | ||
16 | + return ( | ||
17 | + <div> | ||
18 | + <form onSubmit={onBJIDSubmit}> | ||
19 | + <TextField | ||
20 | + name="userBJID" | ||
21 | + onChange={onChange} | ||
22 | + value={profile.userBJID} | ||
23 | + placeholder="백준 아이디" | ||
24 | + label="백준 아이디" | ||
25 | + /> | ||
26 | + <Button variant="outlined" type="submit"> | ||
27 | + 등록 | ||
28 | + </Button> | ||
29 | + </form> | ||
30 | + <Button variant="outlined" onClick={onSyncBJIDSubmit}> | ||
31 | + 동기화 | ||
32 | + </Button> | ||
33 | + </div> | ||
34 | + ); | ||
35 | +}; | ||
36 | +export default BJIDForm; |
1 | +import React from 'react'; | ||
2 | +import palette from '../../lib/styles/palette'; | ||
3 | +import BJIDForm from './BJIDForm'; | ||
4 | +import SlackForm from './SlackForm'; | ||
5 | +import { makeStyles } from '@material-ui/core/styles'; | ||
6 | +import Paper from '@material-ui/core/Paper'; | ||
7 | +import Grid from '@material-ui/core/Grid'; | ||
8 | + | ||
9 | +const useStyles = makeStyles((theme) => ({ | ||
10 | + root: { | ||
11 | + flexGrow: 1, | ||
12 | + background: palette.gray[2], | ||
13 | + }, | ||
14 | + paper: { | ||
15 | + margin: 'auto', | ||
16 | + textAlign: 'center', | ||
17 | + padding: 30, | ||
18 | + }, | ||
19 | +})); | ||
20 | + | ||
21 | +const SettingForm = ({ | ||
22 | + onChange, | ||
23 | + onBJIDSubmit, | ||
24 | + onSlackURLSubmit, | ||
25 | + profile, | ||
26 | + onSyncBJIDSubmit, | ||
27 | +}) => { | ||
28 | + const classes = useStyles(); | ||
29 | + return ( | ||
30 | + <div className={classes.root}> | ||
31 | + <Grid container spacing={3}> | ||
32 | + <Grid item xs={12}> | ||
33 | + <Paper className={classes.paper}> | ||
34 | + <h3>{profile.username}</h3> | ||
35 | + </Paper> | ||
36 | + </Grid> | ||
37 | + <Grid container item xs={12}> | ||
38 | + <Paper className={classes.paper} elevation={3}> | ||
39 | + <BJIDForm | ||
40 | + profile={profile} | ||
41 | + onChange={onChange} | ||
42 | + onBJIDSubmit={onBJIDSubmit} | ||
43 | + onSyncBJIDSubmit={onSyncBJIDSubmit} | ||
44 | + /> | ||
45 | + </Paper> | ||
46 | + </Grid> | ||
47 | + | ||
48 | + <Grid container item xs={12}> | ||
49 | + <Paper className={classes.paper} elevation={3}> | ||
50 | + <SlackForm | ||
51 | + profile={profile} | ||
52 | + onChange={onChange} | ||
53 | + onSlackURLSubmit={onSlackURLSubmit} | ||
54 | + /> | ||
55 | + </Paper> | ||
56 | + </Grid> | ||
57 | + </Grid> | ||
58 | + </div> | ||
59 | + ); | ||
60 | +}; | ||
61 | + | ||
62 | +export default SettingForm; |
1 | +import React from 'react'; | ||
2 | +import { makeStyles } from '@material-ui/core/styles'; | ||
3 | + | ||
4 | +import Button from '@material-ui/core/Button'; | ||
5 | +import TextField from '@material-ui/core/TextField'; | ||
6 | + | ||
7 | +const useStyles = makeStyles((theme) => ({ | ||
8 | + root: { | ||
9 | + '& > *': { | ||
10 | + margin: theme.spacing(1), | ||
11 | + }, | ||
12 | + }, | ||
13 | +})); | ||
14 | + | ||
15 | +const SlackForm = ({ onChange, profile, onSlackURLSubmit }) => { | ||
16 | + const classes = useStyles(); | ||
17 | + return ( | ||
18 | + <div> | ||
19 | + <form onSubmit={onSlackURLSubmit}> | ||
20 | + <TextField | ||
21 | + name="slackWebHookURL" | ||
22 | + onChange={onChange} | ||
23 | + value={profile.slackWebHookURL} | ||
24 | + placeholder="슬랙 Webhook URL" | ||
25 | + label="슬랙 Webhook URL" | ||
26 | + /> | ||
27 | + <Button variant="outlined" type="submit"> | ||
28 | + 등록 | ||
29 | + </Button> | ||
30 | + </form> | ||
31 | + </div> | ||
32 | + ); | ||
33 | +}; | ||
34 | + | ||
35 | +export default SlackForm; |
1 | +import React, { useEffect, useState } from 'react'; | ||
2 | +import { useDispatch, useSelector } from 'react-redux'; | ||
3 | +import { withRouter } from 'react-router-dom'; | ||
4 | +import { changeField, initializeForm, login } from '../../modules/auth'; | ||
5 | +import AuthForm from '../../components/auth/AuthForm'; | ||
6 | +import { check } from '../../modules/user'; | ||
7 | + | ||
8 | +const LoginForm = ({ history }) => { | ||
9 | + const dispatch = useDispatch(); | ||
10 | + const [error, setError] = useState(null); | ||
11 | + const { form, auth, authError, user } = useSelector(({ auth, user }) => ({ | ||
12 | + form: auth.login, | ||
13 | + auth: auth.auth, | ||
14 | + authError: auth.authError, | ||
15 | + user: user.user, | ||
16 | + })); | ||
17 | + | ||
18 | + const onChange = (e) => { | ||
19 | + const { value, name } = e.target; | ||
20 | + dispatch( | ||
21 | + changeField({ | ||
22 | + form: 'login', | ||
23 | + key: name, | ||
24 | + value, | ||
25 | + }), | ||
26 | + ); | ||
27 | + }; | ||
28 | + | ||
29 | + const onSubmit = (e) => { | ||
30 | + e.preventDefault(); | ||
31 | + const { username, password } = form; | ||
32 | + dispatch(login({ username, password })); | ||
33 | + }; | ||
34 | + | ||
35 | + useEffect(() => { | ||
36 | + dispatch(initializeForm('login')); | ||
37 | + }, [dispatch]); | ||
38 | + | ||
39 | + useEffect(() => { | ||
40 | + if (authError) { | ||
41 | + console.log('Error Occured'); | ||
42 | + console.log(authError); | ||
43 | + setError('로그인 실패'); | ||
44 | + return; | ||
45 | + } | ||
46 | + if (auth) { | ||
47 | + console.log('Login Success'); | ||
48 | + dispatch(check()); | ||
49 | + } | ||
50 | + }, [auth, authError, dispatch]); | ||
51 | + | ||
52 | + useEffect(() => { | ||
53 | + if (user) { | ||
54 | + history.push('/'); | ||
55 | + try { | ||
56 | + localStorage.setItem('user', JSON.stringify(user)); | ||
57 | + } catch (e) { | ||
58 | + console.log('localStorage is not working'); | ||
59 | + } | ||
60 | + console.log(user); | ||
61 | + } | ||
62 | + }, [history, user]); | ||
63 | + return ( | ||
64 | + <AuthForm | ||
65 | + type="login" | ||
66 | + form={form} | ||
67 | + onChange={onChange} | ||
68 | + onSubmit={onSubmit} | ||
69 | + error={error} | ||
70 | + ></AuthForm> | ||
71 | + ); | ||
72 | +}; | ||
73 | + | ||
74 | +export default withRouter(LoginForm); |
1 | +import React, { useEffect, useState } from 'react'; | ||
2 | +import { useDispatch, useSelector } from 'react-redux'; | ||
3 | +import { changeField, initializeForm, register } from '../../modules/auth'; | ||
4 | +import AuthForm from '../../components/auth/AuthForm'; | ||
5 | +import { check } from '../../modules/user'; | ||
6 | +import { withRouter } from 'react-router-dom'; | ||
7 | + | ||
8 | +const RegisterForm = ({ history }) => { | ||
9 | + const [error, setError] = useState(null); | ||
10 | + const dispatch = useDispatch(); | ||
11 | + const { form, auth, authError, user } = useSelector(({ auth, user }) => ({ | ||
12 | + form: auth.register, | ||
13 | + auth: auth.auth, | ||
14 | + authError: auth.authError, | ||
15 | + user: user.user, | ||
16 | + })); | ||
17 | + | ||
18 | + const onChange = (e) => { | ||
19 | + const { value, name } = e.target; | ||
20 | + dispatch( | ||
21 | + changeField({ | ||
22 | + form: 'register', | ||
23 | + key: name, | ||
24 | + value, | ||
25 | + }), | ||
26 | + ); | ||
27 | + }; | ||
28 | + | ||
29 | + const onSubmit = (e) => { | ||
30 | + e.preventDefault(); | ||
31 | + const { username, password, passwordConfirm } = form; | ||
32 | + if ([username, password, passwordConfirm].includes('')) { | ||
33 | + setError('빈 칸을 모두 입력하세요'); | ||
34 | + return; | ||
35 | + } | ||
36 | + if (password !== passwordConfirm) { | ||
37 | + setError('비밀번호가 일치하지 않습니다.'); | ||
38 | + changeField({ form: 'register', key: 'password', value: '' }); | ||
39 | + changeField({ form: 'register', key: 'passwordConfirm', value: '' }); | ||
40 | + return; | ||
41 | + } | ||
42 | + dispatch(register({ username, password })); | ||
43 | + }; | ||
44 | + | ||
45 | + useEffect(() => { | ||
46 | + dispatch(initializeForm('register')); | ||
47 | + }, [dispatch]); | ||
48 | + useEffect(() => { | ||
49 | + if (authError) { | ||
50 | + if (authError.response.status === 409) { | ||
51 | + setError('이미 존재하는 계정명입니다.'); | ||
52 | + return; | ||
53 | + } | ||
54 | + setError('회원가입 실패'); | ||
55 | + return; | ||
56 | + } | ||
57 | + | ||
58 | + if (auth) { | ||
59 | + console.log('Register Success!'); | ||
60 | + console.log(auth); | ||
61 | + dispatch(check()); | ||
62 | + } | ||
63 | + }, [auth, authError, dispatch]); | ||
64 | + | ||
65 | + useEffect(() => { | ||
66 | + if (user) { | ||
67 | + console.log('SUCCESS check API'); | ||
68 | + history.push('/'); | ||
69 | + try { | ||
70 | + localStorage.setItem('user', JSON.stringify(user)); | ||
71 | + } catch (e) { | ||
72 | + console.log('localStorage is not working'); | ||
73 | + } | ||
74 | + } | ||
75 | + }, [history, user]); | ||
76 | + return ( | ||
77 | + <AuthForm | ||
78 | + type="register" | ||
79 | + form={form} | ||
80 | + onChange={onChange} | ||
81 | + onSubmit={onSubmit} | ||
82 | + error={error} | ||
83 | + ></AuthForm> | ||
84 | + ); | ||
85 | +}; | ||
86 | + | ||
87 | +export default withRouter(RegisterForm); |
1 | +import React from 'react'; | ||
2 | +import { useSelector, useDispatch } from 'react-redux'; | ||
3 | +import Header from '../../components/common/Header'; | ||
4 | +import { logout } from '../../modules/user'; | ||
5 | +const HeaderContainer = () => { | ||
6 | + const { user } = useSelector(({ user }) => ({ user: user.user })); | ||
7 | + | ||
8 | + const dispatch = useDispatch(); | ||
9 | + const onLogout = () => { | ||
10 | + dispatch(logout()); | ||
11 | + }; | ||
12 | + return <Header user={user} onLogout={onLogout} />; | ||
13 | +}; | ||
14 | +export default HeaderContainer; |
1 | +import React, { useEffect } from 'react'; | ||
2 | +import { useDispatch, useSelector } from 'react-redux'; | ||
3 | +import { withRouter } from 'react-router-dom'; | ||
4 | +import HomeForm from '../../components/home/HomeForm'; | ||
5 | +import { getPROFILE } from '../../modules/profile'; | ||
6 | +import { analyzeBJ } from '../../lib/util/analyzeBJ'; | ||
7 | +const HomeContainer = ({ history }) => { | ||
8 | + const dispatch = useDispatch(); | ||
9 | + const { user, profile } = useSelector(({ user, profile }) => ({ | ||
10 | + user: user.user, | ||
11 | + profile: profile, | ||
12 | + })); | ||
13 | + useEffect(() => {}, [profile.solvedBJ]); | ||
14 | + useEffect(() => { | ||
15 | + if (user) { | ||
16 | + let username = user.username; | ||
17 | + dispatch(getPROFILE({ username })); | ||
18 | + } | ||
19 | + }, [dispatch, user]); | ||
20 | + return <HomeForm />; | ||
21 | +}; | ||
22 | +export default withRouter(HomeContainer); |
1 | +import React, { useEffect } from 'react'; | ||
2 | +import { useDispatch, useSelector } from 'react-redux'; | ||
3 | +import { withRouter } from 'react-router-dom'; | ||
4 | +import { | ||
5 | + changeField, | ||
6 | + setBJID, | ||
7 | + getPROFILE, | ||
8 | + syncBJID, | ||
9 | + initializeProfile, | ||
10 | + setSLACK, | ||
11 | +} from '../../modules/profile'; | ||
12 | +import SettingForm from '../../components/setting/SettingForm'; | ||
13 | + | ||
14 | +const SettingContainer = ({ history }) => { | ||
15 | + const dispatch = useDispatch(); | ||
16 | + const { user, profile } = useSelector(({ user, profile }) => ({ | ||
17 | + user: user.user, | ||
18 | + profile: profile, | ||
19 | + })); | ||
20 | + | ||
21 | + const onChange = (e) => { | ||
22 | + const { value, name } = e.target; | ||
23 | + dispatch( | ||
24 | + changeField({ | ||
25 | + key: name, | ||
26 | + value: value, | ||
27 | + }), | ||
28 | + ); | ||
29 | + }; | ||
30 | + | ||
31 | + const onSyncBJIDSubmit = (e) => { | ||
32 | + e.preventDefault(); | ||
33 | + let username = profile.username; | ||
34 | + dispatch(syncBJID({ username })); | ||
35 | + }; | ||
36 | + const onSlackURLSubmit = (e) => { | ||
37 | + e.preventDefault(); | ||
38 | + let username = profile.username; | ||
39 | + let slackWebHookURL = profile.slackWebHookURL; | ||
40 | + dispatch(setSLACK({ username, slackWebHookURL })); | ||
41 | + }; | ||
42 | + | ||
43 | + const onBJIDSubmit = (e) => { | ||
44 | + e.preventDefault(); | ||
45 | + let username = profile.username; | ||
46 | + let userBJID = profile.userBJID; | ||
47 | + | ||
48 | + dispatch(setBJID({ username, userBJID })); | ||
49 | + }; | ||
50 | + | ||
51 | + useEffect(() => { | ||
52 | + if (!user) { | ||
53 | + alert('로그인이 필요합니다 '); | ||
54 | + history.push('/'); | ||
55 | + } else { | ||
56 | + let username = user.username; | ||
57 | + dispatch(getPROFILE({ username })); | ||
58 | + return () => { | ||
59 | + dispatch(initializeProfile()); | ||
60 | + }; | ||
61 | + } | ||
62 | + }, [dispatch, user, history]); | ||
63 | + | ||
64 | + return ( | ||
65 | + <SettingForm | ||
66 | + type="setting" | ||
67 | + onChange={onChange} | ||
68 | + onBJIDSubmit={onBJIDSubmit} | ||
69 | + onSyncBJIDSubmit={onSyncBJIDSubmit} | ||
70 | + onSlackURLSubmit={onSlackURLSubmit} | ||
71 | + profile={profile} | ||
72 | + ></SettingForm> | ||
73 | + ); | ||
74 | +}; | ||
75 | + | ||
76 | +export default withRouter(SettingContainer); |
... | @@ -5,6 +5,24 @@ body { | ... | @@ -5,6 +5,24 @@ body { |
5 | sans-serif; | 5 | sans-serif; |
6 | -webkit-font-smoothing: antialiased; | 6 | -webkit-font-smoothing: antialiased; |
7 | -moz-osx-font-smoothing: grayscale; | 7 | -moz-osx-font-smoothing: grayscale; |
8 | + box-sizing: border-box; | ||
9 | + min-height: 100%; | ||
10 | +} | ||
11 | + | ||
12 | +#root { | ||
13 | + min-height: 100%; | ||
14 | +} | ||
15 | + | ||
16 | +html { | ||
17 | + height: 100%; | ||
18 | +} | ||
19 | + | ||
20 | +a { | ||
21 | + color: inherit; | ||
22 | + text-decoration: none; | ||
23 | +} | ||
24 | +* { | ||
25 | + box-sizing: inherit; | ||
8 | } | 26 | } |
9 | 27 | ||
10 | code { | 28 | code { | ... | ... |
... | @@ -3,12 +3,41 @@ import ReactDOM from 'react-dom'; | ... | @@ -3,12 +3,41 @@ import ReactDOM from 'react-dom'; |
3 | import './index.css'; | 3 | import './index.css'; |
4 | import App from './App'; | 4 | import App from './App'; |
5 | import * as serviceWorker from './serviceWorker'; | 5 | import * as serviceWorker from './serviceWorker'; |
6 | +import { BrowserRouter } from 'react-router-dom'; | ||
7 | +import { Provider } from 'react-redux'; | ||
8 | +import { createStore, applyMiddleware } from 'redux'; | ||
9 | +import { composeWithDevTools } from 'redux-devtools-extension'; | ||
10 | +import createSagaMiddleware from 'redux-saga'; | ||
11 | +import rootReducer, { rootSaga } from './modules'; | ||
12 | +import { tempSetUser, check } from './modules/user'; | ||
13 | + | ||
14 | +const sagaMiddleware = createSagaMiddleware(); | ||
15 | +const store = createStore( | ||
16 | + rootReducer, | ||
17 | + composeWithDevTools(applyMiddleware(sagaMiddleware)), | ||
18 | +); | ||
19 | + | ||
20 | +function loadUser() { | ||
21 | + try { | ||
22 | + const user = localStorage.getItem('user'); | ||
23 | + if (!user) return; | ||
24 | + | ||
25 | + store.dispatch(tempSetUser(user)); | ||
26 | + store.dispatch(check()); | ||
27 | + } catch (e) { | ||
28 | + console.log('localStorage is not working'); | ||
29 | + } | ||
30 | +} | ||
31 | +sagaMiddleware.run(rootSaga); | ||
32 | +loadUser(); | ||
6 | 33 | ||
7 | ReactDOM.render( | 34 | ReactDOM.render( |
8 | - <React.StrictMode> | 35 | + <Provider store={store}> |
9 | - <App /> | 36 | + <BrowserRouter> |
10 | - </React.StrictMode>, | 37 | + <App /> |
11 | - document.getElementById('root') | 38 | + </BrowserRouter> |
39 | + </Provider>, | ||
40 | + document.getElementById('root'), | ||
12 | ); | 41 | ); |
13 | 42 | ||
14 | // If you want your app to work offline and load faster, you can change | 43 | // If you want your app to work offline and load faster, you can change | ... | ... |
jaksimsamil-page/src/lib/api/auth.js
0 → 100644
1 | +import client from './client'; | ||
2 | + | ||
3 | +export const login = ({ username, password }) => | ||
4 | + client.post('api/auth/login', { username, password }); | ||
5 | + | ||
6 | +export const register = ({ username, password }) => | ||
7 | + client.post('api/auth/register', { username, password }); | ||
8 | + | ||
9 | +export const check = () => client.get('api/auth/check'); | ||
10 | + | ||
11 | +export const logout = () => client.post('/api/auth/logout'); |
jaksimsamil-page/src/lib/api/client.js
0 → 100644
jaksimsamil-page/src/lib/api/profile.js
0 → 100644
1 | +import client from './client'; | ||
2 | + | ||
3 | +export const setBJID = ({ username, userBJID }) => | ||
4 | + client.post('api/profile/setprofile', { | ||
5 | + username: username, | ||
6 | + userBJID: userBJID, | ||
7 | + }); | ||
8 | +export const setPROFILE = (postdata) => | ||
9 | + client.post('api/profile/setprofile', postdata); | ||
10 | +export const getPROFILE = ({ username }) => | ||
11 | + client.post('api/profile/getprofile', { username }); | ||
12 | + | ||
13 | +export const syncBJ = ({ username }) => | ||
14 | + client.patch('api/profile/syncBJ', { username }); |
1 | +import { call, put } from 'redux-saga/effects'; | ||
2 | +import { startLoading, finishLoading } from '../modules/loading'; | ||
3 | + | ||
4 | +export const createRequestActionTypes = (type) => { | ||
5 | + const SUCCESS = `${type}_SUCCESS`; | ||
6 | + const FAILURE = `${type}_FAILURE`; | ||
7 | + return [type, SUCCESS, FAILURE]; | ||
8 | +}; | ||
9 | + | ||
10 | +export default function createRequestSaga(type, request) { | ||
11 | + const SUCCESS = `${type}_SUCCESS`; | ||
12 | + const FAILURE = `${type}_FAILURE`; | ||
13 | + | ||
14 | + return function* (action) { | ||
15 | + yield put(startLoading(type)); | ||
16 | + try { | ||
17 | + const response = yield call(request, action.payload); | ||
18 | + yield put({ | ||
19 | + type: SUCCESS, | ||
20 | + payload: response.data, | ||
21 | + }); | ||
22 | + } catch (e) { | ||
23 | + yield put({ | ||
24 | + type: FAILURE, | ||
25 | + payload: e, | ||
26 | + error: true, | ||
27 | + }); | ||
28 | + } | ||
29 | + yield put(finishLoading(type)); | ||
30 | + }; | ||
31 | +} |
jaksimsamil-page/src/lib/styles/palette.js
0 → 100644
1 | +// source: https://yeun.github.io/open-color/ | ||
2 | + | ||
3 | +const palette = { | ||
4 | + gray: [ | ||
5 | + '#f8f9fa', | ||
6 | + '#f1f3f5', | ||
7 | + '#e9ecef', | ||
8 | + '#dee2e6', | ||
9 | + '#ced4da', | ||
10 | + '#adb5bd', | ||
11 | + '#868e96', | ||
12 | + '#495057', | ||
13 | + '#343a40', | ||
14 | + '#212529', | ||
15 | + ], | ||
16 | + cyan: [ | ||
17 | + '#e3fafc', | ||
18 | + '#c5f6fa', | ||
19 | + '#99e9f2', | ||
20 | + '#66d9e8', | ||
21 | + '#3bc9db', | ||
22 | + '#22b8cf', | ||
23 | + '#15aabf', | ||
24 | + '#1098ad', | ||
25 | + '#0c8599', | ||
26 | + '#0b7285', | ||
27 | + ], | ||
28 | +}; | ||
29 | + | ||
30 | +export default palette; |
jaksimsamil-page/src/logo.svg
deleted
100644 → 0
1 | -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> | ||
2 | - <g fill="#61DAFB"> | ||
3 | - <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> | ||
4 | - <circle cx="420.9" cy="296.5" r="45.7"/> | ||
5 | - <path d="M520.5 78.1z"/> | ||
6 | - </g> | ||
7 | -</svg> |
jaksimsamil-page/src/modules/auth.js
0 → 100644
1 | +import { createAction, handleActions } from 'redux-actions'; | ||
2 | +import produce from 'immer'; | ||
3 | +import { takeLatest } from 'redux-saga/effects'; | ||
4 | +import createRequestSaga, { | ||
5 | + createRequestActionTypes, | ||
6 | +} from '../lib/createRequestSaga'; | ||
7 | +import * as authAPI from '../lib/api/auth'; | ||
8 | +const CHANGE_FIELD = 'auth/CHANGE_FIELD'; | ||
9 | +const INITIALIZE_FORM = 'auth/INITIALIZE_FORM'; | ||
10 | + | ||
11 | +const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] = createRequestActionTypes( | ||
12 | + 'auth/REGISTER', | ||
13 | +); | ||
14 | + | ||
15 | +const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] = createRequestActionTypes( | ||
16 | + 'auth/REGISTER', | ||
17 | +); | ||
18 | + | ||
19 | +export const changeField = createAction( | ||
20 | + CHANGE_FIELD, | ||
21 | + ({ form, key, value }) => ({ | ||
22 | + form, | ||
23 | + key, | ||
24 | + value, | ||
25 | + }), | ||
26 | +); | ||
27 | +export const initializeForm = createAction(INITIALIZE_FORM, (form) => form); | ||
28 | + | ||
29 | +const initalState = { | ||
30 | + register: { | ||
31 | + username: '', | ||
32 | + password: '', | ||
33 | + passwordConfirm: '', | ||
34 | + }, | ||
35 | + login: { | ||
36 | + username: '', | ||
37 | + password: '', | ||
38 | + }, | ||
39 | + auth: null, | ||
40 | + authError: null, | ||
41 | +}; | ||
42 | + | ||
43 | +export const register = createAction(REGISTER, ({ username, password }) => ({ | ||
44 | + username, | ||
45 | + password, | ||
46 | +})); | ||
47 | +export const login = createAction(LOGIN, ({ username, password }) => ({ | ||
48 | + username, | ||
49 | + password, | ||
50 | +})); | ||
51 | + | ||
52 | +const registerSaga = createRequestSaga(REGISTER, authAPI.register); | ||
53 | +const loginSaga = createRequestSaga(LOGIN, authAPI.login); | ||
54 | + | ||
55 | +export function* authSaga() { | ||
56 | + yield takeLatest(REGISTER, registerSaga); | ||
57 | + yield takeLatest(LOGIN, loginSaga); | ||
58 | +} | ||
59 | + | ||
60 | +const auth = handleActions( | ||
61 | + { | ||
62 | + [CHANGE_FIELD]: (state, { payload: { form, key, value } }) => | ||
63 | + produce(state, (draft) => { | ||
64 | + draft[form][key] = value; | ||
65 | + }), | ||
66 | + [INITIALIZE_FORM]: (state, { payload: form }) => ({ | ||
67 | + ...state, | ||
68 | + [form]: initalState[form], | ||
69 | + authError: null, | ||
70 | + }), | ||
71 | + [REGISTER_SUCCESS]: (state, { payload: auth }) => ({ | ||
72 | + ...state, | ||
73 | + authError: null, | ||
74 | + auth, | ||
75 | + }), | ||
76 | + [REGISTER_FAILURE]: (state, { payload: error }) => ({ | ||
77 | + ...state, | ||
78 | + authError: error, | ||
79 | + }), | ||
80 | + [LOGIN_SUCCESS]: (state, { payload: auth }) => ({ | ||
81 | + ...state, | ||
82 | + authError: null, | ||
83 | + auth, | ||
84 | + }), | ||
85 | + [LOGIN_FAILURE]: (state, { payload: error }) => ({ | ||
86 | + ...state, | ||
87 | + authError: error, | ||
88 | + }), | ||
89 | + }, | ||
90 | + initalState, | ||
91 | +); | ||
92 | + | ||
93 | +export default auth; |
jaksimsamil-page/src/modules/index.js
0 → 100644
1 | +import { combineReducers } from 'redux'; | ||
2 | +import { all } from 'redux-saga/effects'; | ||
3 | +import auth, { authSaga } from './auth'; | ||
4 | +import loading from './loading'; | ||
5 | +import user, { userSaga } from './user'; | ||
6 | +import profile, { profileSaga } from './profile'; | ||
7 | + | ||
8 | +const rootReducer = combineReducers({ | ||
9 | + auth, | ||
10 | + loading, | ||
11 | + user, | ||
12 | + profile, | ||
13 | +}); | ||
14 | + | ||
15 | +export function* rootSaga() { | ||
16 | + yield all([authSaga(), userSaga(), profileSaga()]); | ||
17 | +} | ||
18 | + | ||
19 | +export default rootReducer; |
jaksimsamil-page/src/modules/loading.js
0 → 100644
1 | +import { createAction, handleActions } from 'redux-actions'; | ||
2 | + | ||
3 | +const START_LOADING = 'loading/START_LOADING'; | ||
4 | +const FINISH_LOADING = 'loading/FINISH_LOADING'; | ||
5 | + | ||
6 | +export const startLoading = createAction( | ||
7 | + START_LOADING, | ||
8 | + (requestType) => requestType, | ||
9 | +); | ||
10 | + | ||
11 | +export const finishLoading = createAction( | ||
12 | + FINISH_LOADING, | ||
13 | + (requestType) => requestType, | ||
14 | +); | ||
15 | + | ||
16 | +const initialState = {}; | ||
17 | + | ||
18 | +const loading = handleActions( | ||
19 | + { | ||
20 | + [START_LOADING]: (state, action) => ({ | ||
21 | + ...state, | ||
22 | + [action.payload]: true, | ||
23 | + }), | ||
24 | + [FINISH_LOADING]: (state, action) => ({ | ||
25 | + ...state, | ||
26 | + [action.payload]: false, | ||
27 | + }), | ||
28 | + }, | ||
29 | + initialState, | ||
30 | +); | ||
31 | + | ||
32 | +export default loading; |
jaksimsamil-page/src/modules/profile.js
0 → 100644
1 | +import { createAction, handleActions } from 'redux-actions'; | ||
2 | +import createRequestSaga, { | ||
3 | + createRequestActionTypes, | ||
4 | +} from '../lib/createRequestSaga'; | ||
5 | +import produce from 'immer'; | ||
6 | +import * as profileAPI from '../lib/api/profile'; | ||
7 | +import { takeLatest } from 'redux-saga/effects'; | ||
8 | + | ||
9 | +const INITIALIZE = 'profile/INITIALIZE'; | ||
10 | +const CHANGE_FIELD = 'profile/CHANGE_FIELD'; | ||
11 | +const [SET_BJID, SET_BJID_SUCCESS, SET_BJID_FAILURE] = createRequestActionTypes( | ||
12 | + 'profile/SET_BJID', | ||
13 | +); | ||
14 | +const [ | ||
15 | + SET_SLACK, | ||
16 | + SET_SLACK_SUCCESS, | ||
17 | + SET_SLACK_FAILURE, | ||
18 | +] = createRequestActionTypes('/profile/SET_SLACK'); | ||
19 | +const [ | ||
20 | + GET_PROFILE, | ||
21 | + GET_PROFILE_SUCCESS, | ||
22 | + GET_PROFILE_FAILURE, | ||
23 | +] = createRequestActionTypes('profile/GET_PROFILE'); | ||
24 | + | ||
25 | +const [ | ||
26 | + SYNC_BJID, | ||
27 | + SYNC_BJID_SUCCESS, | ||
28 | + SYNC_BJID_FAILURE, | ||
29 | +] = createRequestActionTypes('profile/SYNC_BJID'); | ||
30 | +export const initializeProfile = createAction(INITIALIZE); | ||
31 | +export const syncBJID = createAction(SYNC_BJID, ({ username }) => ({ | ||
32 | + username, | ||
33 | +})); | ||
34 | +export const setSLACK = createAction( | ||
35 | + SET_SLACK, | ||
36 | + ({ username, slackWebHookURL }) => ({ | ||
37 | + username, | ||
38 | + slackWebHookURL, | ||
39 | + }), | ||
40 | +); | ||
41 | +export const setBJID = createAction(SET_BJID, ({ username, userBJID }) => ({ | ||
42 | + username, | ||
43 | + userBJID, | ||
44 | +})); | ||
45 | + | ||
46 | +export const changeField = createAction(CHANGE_FIELD, ({ key, value }) => ({ | ||
47 | + key, | ||
48 | + value, | ||
49 | +})); | ||
50 | + | ||
51 | +export const getPROFILE = createAction(GET_PROFILE, ({ username }) => ({ | ||
52 | + username, | ||
53 | +})); | ||
54 | +const initialState = { | ||
55 | + username: '', | ||
56 | + userBJID: '', | ||
57 | + solvedBJ: '', | ||
58 | + friendList: [], | ||
59 | + profileError: '', | ||
60 | + slackWebHookURL: '', | ||
61 | +}; | ||
62 | +const getPROFILESaga = createRequestSaga(GET_PROFILE, profileAPI.getPROFILE); | ||
63 | +const setBJIDSaga = createRequestSaga(SET_BJID, profileAPI.setBJID); | ||
64 | +const setSLACKSaga = createRequestSaga(SET_SLACK, profileAPI.setPROFILE); | ||
65 | +const syncBJIDSaga = createRequestSaga(SYNC_BJID, profileAPI.syncBJ); | ||
66 | +export function* profileSaga() { | ||
67 | + yield takeLatest(SET_BJID, setBJIDSaga); | ||
68 | + yield takeLatest(GET_PROFILE, getPROFILESaga); | ||
69 | + yield takeLatest(SYNC_BJID, syncBJIDSaga); | ||
70 | + yield takeLatest(SET_SLACK, setSLACKSaga); | ||
71 | +} | ||
72 | + | ||
73 | +export default handleActions( | ||
74 | + { | ||
75 | + [INITIALIZE]: (state) => initialState, | ||
76 | + [CHANGE_FIELD]: (state, { payload: { key, value } }) => | ||
77 | + produce(state, (draft) => { | ||
78 | + draft[key] = value; | ||
79 | + }), | ||
80 | + [GET_PROFILE_SUCCESS]: ( | ||
81 | + state, | ||
82 | + { | ||
83 | + payload: { username, userBJID, solvedBJ, friendList, slackWebHookURL }, | ||
84 | + }, | ||
85 | + ) => ({ | ||
86 | + ...state, | ||
87 | + username: username, | ||
88 | + userBJID: userBJID, | ||
89 | + solvedBJ: solvedBJ, | ||
90 | + friendList: friendList, | ||
91 | + profileError: null, | ||
92 | + slackWebHookURL: slackWebHookURL, | ||
93 | + }), | ||
94 | + [GET_PROFILE_FAILURE]: (state, { payload: error }) => ({ | ||
95 | + ...state, | ||
96 | + profileError: error, | ||
97 | + }), | ||
98 | + | ||
99 | + [SET_BJID_SUCCESS]: (state, { payload: { userBJID } }) => ({ | ||
100 | + ...state, | ||
101 | + userBJID: userBJID, | ||
102 | + profileError: null, | ||
103 | + }), | ||
104 | + [SET_BJID_FAILURE]: (state, { payload: error }) => ({ | ||
105 | + ...state, | ||
106 | + profileError: error, | ||
107 | + }), | ||
108 | + [SET_SLACK_SUCCESS]: (state, { payload: { slackWebHookURL } }) => ({ | ||
109 | + ...state, | ||
110 | + slackWebHookURL: slackWebHookURL, | ||
111 | + profileError: null, | ||
112 | + }), | ||
113 | + [SET_SLACK_FAILURE]: (state, { payload: error }) => ({ | ||
114 | + ...state, | ||
115 | + profileError: error, | ||
116 | + }), | ||
117 | + [SYNC_BJID_SUCCESS]: (state, { payload: { solvedBJ } }) => ({ | ||
118 | + ...state, | ||
119 | + solvedBJ, | ||
120 | + profileError: null, | ||
121 | + }), | ||
122 | + [SYNC_BJID_FAILURE]: (state, { payload: error }) => ({ | ||
123 | + ...state, | ||
124 | + profileError: error, | ||
125 | + }), | ||
126 | + }, | ||
127 | + initialState, | ||
128 | +); |
jaksimsamil-page/src/modules/user.js
0 → 100644
1 | +import { createAction, handleActions } from 'redux-actions'; | ||
2 | +import { takeLatest, call } from 'redux-saga/effects'; | ||
3 | +import * as authAPI from '../lib/api/auth'; | ||
4 | +import createRequestSaga, { | ||
5 | + createRequestActionTypes, | ||
6 | +} from '../lib/createRequestSaga'; | ||
7 | + | ||
8 | +const TEMP_SET_USER = 'user/TEMP_SET_USER'; | ||
9 | +const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] = createRequestActionTypes( | ||
10 | + 'user/CHECK', | ||
11 | +); | ||
12 | +const LOGOUT = 'user/LOGOUT'; | ||
13 | + | ||
14 | +export const tempSetUser = createAction(TEMP_SET_USER, (user) => user); | ||
15 | +export const check = createAction(CHECK); | ||
16 | +export const logout = createAction(LOGOUT); | ||
17 | +const checkSaga = createRequestSaga(CHECK, authAPI.check); | ||
18 | +function checkFailureSaga() { | ||
19 | + try { | ||
20 | + localStorage.removeItem('user'); | ||
21 | + } catch (e) { | ||
22 | + console.log('localStroage is not working'); | ||
23 | + } | ||
24 | +} | ||
25 | +function* logoutSaga() { | ||
26 | + try { | ||
27 | + yield call(authAPI.logout); | ||
28 | + console.log('logout'); | ||
29 | + localStorage.removeItem('user'); | ||
30 | + } catch (e) { | ||
31 | + console.log(e); | ||
32 | + } | ||
33 | +} | ||
34 | +export function* userSaga() { | ||
35 | + yield takeLatest(CHECK, checkSaga); | ||
36 | + yield takeLatest(CHECK_FAILURE, checkFailureSaga); | ||
37 | + yield takeLatest(LOGOUT, logoutSaga); | ||
38 | +} | ||
39 | + | ||
40 | +const initialState = { | ||
41 | + user: null, | ||
42 | + checkError: null, | ||
43 | +}; | ||
44 | + | ||
45 | +export default handleActions( | ||
46 | + { | ||
47 | + [TEMP_SET_USER]: (state, { payload: user }) => ({ | ||
48 | + ...state, | ||
49 | + user, | ||
50 | + }), | ||
51 | + [CHECK_SUCCESS]: (state, { payload: user }) => ({ | ||
52 | + ...state, | ||
53 | + user, | ||
54 | + checkError: null, | ||
55 | + }), | ||
56 | + [CHECK_FAILURE]: (state, { payload: error }) => ({ | ||
57 | + ...state, | ||
58 | + user: null, | ||
59 | + checkError: error, | ||
60 | + }), | ||
61 | + [LOGOUT]: (state) => ({ | ||
62 | + ...state, | ||
63 | + user: null, | ||
64 | + }), | ||
65 | + }, | ||
66 | + initialState, | ||
67 | +); |
jaksimsamil-page/src/pages/HomePage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import HeaderContainer from '../containers/common/HeaderContainer'; | ||
3 | +import HomeContainer from '../containers/home/HomeContainer'; | ||
4 | + | ||
5 | +const HomePage = () => { | ||
6 | + return ( | ||
7 | + <div> | ||
8 | + <HeaderContainer /> | ||
9 | + <HomeContainer /> | ||
10 | + </div> | ||
11 | + ); | ||
12 | +}; | ||
13 | + | ||
14 | +export default HomePage; |
jaksimsamil-page/src/pages/LoginPage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import AuthTemplate from '../components/auth/AuthTemplate'; | ||
3 | +import LoginForm from '../containers/auth/LoginForm'; | ||
4 | + | ||
5 | +const LoginPage = () => { | ||
6 | + return ( | ||
7 | + <AuthTemplate> | ||
8 | + <LoginForm type="login" /> | ||
9 | + </AuthTemplate> | ||
10 | + ); | ||
11 | +}; | ||
12 | + | ||
13 | +export default LoginPage; |
jaksimsamil-page/src/pages/RegisterPage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import AuthTemplate from '../components/auth/AuthTemplate'; | ||
3 | +import RegisterForm from '../containers/auth/RegisterForm'; | ||
4 | + | ||
5 | +const RegisterPage = () => { | ||
6 | + return ( | ||
7 | + <AuthTemplate> | ||
8 | + <RegisterForm type="register" /> | ||
9 | + </AuthTemplate> | ||
10 | + ); | ||
11 | +}; | ||
12 | + | ||
13 | +export default RegisterPage; |
jaksimsamil-page/src/pages/SettingPage.js
0 → 100644
1 | +import React from 'react'; | ||
2 | +import HeaderContainer from '../containers/common/HeaderContainer'; | ||
3 | +import SettingContainer from '../containers/setting/SettingContainer'; | ||
4 | + | ||
5 | +const SettingPage = () => { | ||
6 | + return ( | ||
7 | + <div> | ||
8 | + <HeaderContainer /> | ||
9 | + <SettingContainer></SettingContainer> | ||
10 | + </div> | ||
11 | + ); | ||
12 | +}; | ||
13 | + | ||
14 | +export default SettingPage; |
This diff could not be displayed because it is too large.
... | @@ -18,20 +18,22 @@ | ... | @@ -18,20 +18,22 @@ |
18 | 18 | ||
19 | ## API Table | 19 | ## API Table |
20 | 20 | ||
21 | -| group | description | method | URL | Detail | Auth | | 21 | +| group | description | method | URL | Detail | Auth | |
22 | -| ------- | --------------------------- | ------ | ------------------------ | -------- | --------- | | 22 | +| ------- | --------------------------- | --------- | ------------------------ | -------- | --------- | |
23 | -| user | 유저 등록 | POST | api/user | 바로가기 | JWT Token | | 23 | +| user | 유저 등록 | POST | api/user | 바로가기 | JWT Token | |
24 | -| user | 유저 삭제 | DELETE | api/user:id | 바로가기 | JWT Token | | 24 | +| user | 유저 삭제 | DELETE | api/user:id | 바로가기 | JWT Token | |
25 | -| user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None | | 25 | +| user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None | |
26 | -| user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token | | 26 | +| user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token | |
27 | -| friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token | | 27 | +| friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token | |
28 | -| friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None | | 28 | +| friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None | |
29 | -| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | 바로가기 | None | | 29 | +| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | 바로가기 | None | |
30 | -| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | 바로가기 | None | | 30 | +| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | 바로가기 | None | |
31 | -| profile | 유저 정보 수정 | POST | api/profile/setprofile | 바로가기 | JWT TOKEN | | 31 | +| profile | 유저 정보 수정 | POST | api/profile/setprofile | 바로가기 | JWT TOKEN | |
32 | -| profile | 추천 문제 조회 | GET | api/profile/recommend:id | 바로가기 | None | | 32 | +| profile | 유저 정보 받아오기 | POST | api/profile/getprofile | 바로가기 | JWT | |
33 | -| notify | 슬랙 메시지 전송 요청 | POST | api/notify/slack | 바로가기 | Jwt Token | | 33 | +| profile | 추천 문제 조회 | GET | api/profile/recommend:id | 바로가기 | None | |
34 | -| auth | 로그인 | POST | api/auth/login | 바로가기 | None | | 34 | +| notify | 슬랙 메시지 전송 요청 | POST | api/notify/ | |
35 | -| auth | 로그아웃 | GET | api/auth/logout | 바로가기 | JWT Token | | 35 | +| slack | 바로가기 | Jwt Token | |
36 | -| auth | 회원가입 | POST | api/auth/register | 바로가기 | None | | 36 | +| auth | 로그인 | POST | api/auth/login | 바로가기 | None | |
37 | -| auth | 로그인 확인 | GET | api/auth/check | 바로가기 | None | | 37 | +| auth | 로그아웃 | POST | api/auth/logout | 바로가기 | JWT Token | |
38 | +| auth | 회원가입 | POST | api/auth/register | 바로가기 | None | | ||
39 | +| auth | 로그인 확인 | GET | api/auth/check | 바로가기 | None | | ... | ... |
jaksimsamil-server/access.log
0 → 100644
This diff could not be displayed because it is too large.
... | @@ -5,6 +5,10 @@ const mongoose = require("mongoose"); | ... | @@ -5,6 +5,10 @@ const mongoose = require("mongoose"); |
5 | const fs = require("fs"); | 5 | const fs = require("fs"); |
6 | const morgan = require("koa-morgan"); | 6 | const morgan = require("koa-morgan"); |
7 | const jwtMiddleware = require("./src/lib/jwtMiddleware"); | 7 | const jwtMiddleware = require("./src/lib/jwtMiddleware"); |
8 | +const api = require("./src/api"); | ||
9 | + | ||
10 | +require("dotenv").config(); | ||
11 | + | ||
8 | const app = new Koa(); | 12 | const app = new Koa(); |
9 | const router = new Router(); | 13 | const router = new Router(); |
10 | const accessLogStream = fs.createWriteStream(__dirname + "/access.log", { | 14 | const accessLogStream = fs.createWriteStream(__dirname + "/access.log", { |
... | @@ -14,7 +18,6 @@ require("dotenv").config(); | ... | @@ -14,7 +18,6 @@ require("dotenv").config(); |
14 | app.use(bodyParser()); | 18 | app.use(bodyParser()); |
15 | app.use(jwtMiddleware); | 19 | app.use(jwtMiddleware); |
16 | app.use(morgan("combined", { stream: accessLogStream })); | 20 | app.use(morgan("combined", { stream: accessLogStream })); |
17 | -const api = require("./src/api"); | ||
18 | const { SERVER_PORT, MONGO_URL } = process.env; | 21 | const { SERVER_PORT, MONGO_URL } = process.env; |
19 | 22 | ||
20 | router.use("/api", api.routes()); | 23 | router.use("/api", api.routes()); |
... | @@ -32,6 +35,7 @@ mongoose | ... | @@ -32,6 +35,7 @@ mongoose |
32 | .catch((e) => { | 35 | .catch((e) => { |
33 | console.log(e); | 36 | console.log(e); |
34 | }); | 37 | }); |
38 | + | ||
35 | app.listen(SERVER_PORT, () => { | 39 | app.listen(SERVER_PORT, () => { |
36 | console.log("Server is running on port", process.env.SERVER_PORT); | 40 | console.log("Server is running on port", process.env.SERVER_PORT); |
37 | }); | 41 | }); | ... | ... |
This diff is collapsed. Click to expand it.
... | @@ -21,7 +21,10 @@ | ... | @@ -21,7 +21,10 @@ |
21 | "koa-router": "^9.0.1", | 21 | "koa-router": "^9.0.1", |
22 | "mongoose": "^5.9.17", | 22 | "mongoose": "^5.9.17", |
23 | "morgan": "^1.10.0", | 23 | "morgan": "^1.10.0", |
24 | + "node-schedule": "^1.3.2", | ||
24 | "path": "^0.12.7", | 25 | "path": "^0.12.7", |
26 | + "slack-client": "^2.0.6", | ||
27 | + "slack-node": "^0.1.8", | ||
25 | "voca": "^1.4.0" | 28 | "voca": "^1.4.0" |
26 | }, | 29 | }, |
27 | "devDependencies": { | 30 | "devDependencies": { | ... | ... |
... | @@ -40,7 +40,7 @@ exports.register = async (ctx) => { | ... | @@ -40,7 +40,7 @@ exports.register = async (ctx) => { |
40 | ctx.body = user.serialize(); | 40 | ctx.body = user.serialize(); |
41 | 41 | ||
42 | const token = user.generateToken(); | 42 | const token = user.generateToken(); |
43 | - ctx.cookies.set("acces_token", token, { | 43 | + ctx.cookies.set("access_token", token, { |
44 | //3일동안 유효 | 44 | //3일동안 유효 |
45 | maxAge: 1000 * 60 * 60 * 24 * 3, | 45 | maxAge: 1000 * 60 * 60 * 24 * 3, |
46 | httpOnly: true, | 46 | httpOnly: true, |
... | @@ -75,7 +75,7 @@ exports.login = async (ctx) => { | ... | @@ -75,7 +75,7 @@ exports.login = async (ctx) => { |
75 | } | 75 | } |
76 | ctx.body = user.serialize(); | 76 | ctx.body = user.serialize(); |
77 | const token = user.generateToken(); | 77 | const token = user.generateToken(); |
78 | - ctx.cookies.set("acces_token", token, { | 78 | + ctx.cookies.set("access_token", token, { |
79 | //7일동안 유효 | 79 | //7일동안 유효 |
80 | maxAge: 1000 * 60 * 60 * 24 * 7, | 80 | maxAge: 1000 * 60 * 60 * 24 * 7, |
81 | httpOnly: true, | 81 | httpOnly: true, | ... | ... |
... | @@ -2,7 +2,8 @@ const Router = require("koa-router"); | ... | @@ -2,7 +2,8 @@ const Router = require("koa-router"); |
2 | const auth = new Router(); | 2 | const auth = new Router(); |
3 | const authCtrl = require("./auth.ctrl"); | 3 | const authCtrl = require("./auth.ctrl"); |
4 | auth.post("/login", authCtrl.login); | 4 | auth.post("/login", authCtrl.login); |
5 | -auth.get("/logout", authCtrl.logout); | 5 | +auth.post("/logout", authCtrl.logout); |
6 | auth.post("/register", authCtrl.register); | 6 | auth.post("/register", authCtrl.register); |
7 | +auth.get("/check", authCtrl.check); | ||
7 | 8 | ||
8 | module.exports = auth; | 9 | module.exports = auth; | ... | ... |
... | @@ -7,4 +7,5 @@ profile.get("/solvednum:id"); | ... | @@ -7,4 +7,5 @@ profile.get("/solvednum:id"); |
7 | profile.get("/recommendps:id"); | 7 | profile.get("/recommendps:id"); |
8 | profile.patch("/syncBJ", profileCtrl.syncBJ); | 8 | profile.patch("/syncBJ", profileCtrl.syncBJ); |
9 | profile.post("/setprofile", profileCtrl.setProfile); | 9 | profile.post("/setprofile", profileCtrl.setProfile); |
10 | +profile.post("/getprofile", profileCtrl.getProfile); | ||
10 | module.exports = profile; | 11 | module.exports = profile; | ... | ... |
... | @@ -13,6 +13,24 @@ exports.checkObjectId = (ctx, next) => { | ... | @@ -13,6 +13,24 @@ exports.checkObjectId = (ctx, next) => { |
13 | } | 13 | } |
14 | return next(); | 14 | return next(); |
15 | }; | 15 | }; |
16 | +/*POST /api/profile/getprofile | ||
17 | +{ | ||
18 | + username: "username" | ||
19 | +} | ||
20 | +*/ | ||
21 | +exports.getProfile = async (ctx) => { | ||
22 | + try { | ||
23 | + const { username } = ctx.request.body; | ||
24 | + const profile = await Profile.findByUsername(username); | ||
25 | + if (!profile) { | ||
26 | + ctx.status = 401; | ||
27 | + return; | ||
28 | + } | ||
29 | + ctx.body = profile; | ||
30 | + } catch (e) { | ||
31 | + ctx.throw(500, e); | ||
32 | + } | ||
33 | +}; | ||
16 | /* | 34 | /* |
17 | POST /api/proflie/setprofile | 35 | POST /api/proflie/setprofile |
18 | { | 36 | { | ... | ... |
jaksimsamil-server/src/data/problem_set.js
0 → 100644
1 | +export const problem_set = [ | ||
2 | + "1517", | ||
3 | + "2448", | ||
4 | + "1891", | ||
5 | + "1074", | ||
6 | + "2263", | ||
7 | + "1780", | ||
8 | + "11728", | ||
9 | + "10816", | ||
10 | + "10815", | ||
11 | + "2109", | ||
12 | + "1202", | ||
13 | + "1285", | ||
14 | + "2138", | ||
15 | + "1080", | ||
16 | + "11399", | ||
17 | + "1931", | ||
18 | + "11047", | ||
19 | + "15666", | ||
20 | + "15665", | ||
21 | + "15664", | ||
22 | + "15663", | ||
23 | + "15657", | ||
24 | + "15656", | ||
25 | + "15655", | ||
26 | + "15654", | ||
27 | + "15652", | ||
28 | + "15651", | ||
29 | + "15650", | ||
30 | + "15649", | ||
31 | + "6603", | ||
32 | + "10971", | ||
33 | + "10819", | ||
34 | + "10973", | ||
35 | + "10974", | ||
36 | + "10972", | ||
37 | + "7576", | ||
38 | + "1248", | ||
39 | + "2529", | ||
40 | + "15661", | ||
41 | + "14501", | ||
42 | + "1759", | ||
43 | + "14391", | ||
44 | + "14889", | ||
45 | + "1182", | ||
46 | + "11723", | ||
47 | + "1748", | ||
48 | + "6064", | ||
49 | + "1107", | ||
50 | + "3085", | ||
51 | + "2309", | ||
52 | + "1748", | ||
53 | + "14500", | ||
54 | + "1107", | ||
55 | + "1476", | ||
56 | + "3085", | ||
57 | + "2309", | ||
58 | + "1261", | ||
59 | + "13549", | ||
60 | + "14226", | ||
61 | + "13913", | ||
62 | + "1697", | ||
63 | + "1967", | ||
64 | + "1167", | ||
65 | + "11725", | ||
66 | + "2250", | ||
67 | + "1991", | ||
68 | + "7562", | ||
69 | + "2178", | ||
70 | + "4963", | ||
71 | + "2667", | ||
72 | + "1707", | ||
73 | + "11724", | ||
74 | + "1260", | ||
75 | + "13023", | ||
76 | + "11652", | ||
77 | + "1377", | ||
78 | + "11004", | ||
79 | + "10825", | ||
80 | + "2751", | ||
81 | + "9461", | ||
82 | + "1699", | ||
83 | + "9095", | ||
84 | + "2225", | ||
85 | + "2133", | ||
86 | + "11727", | ||
87 | + "11726", | ||
88 | + "1463", | ||
89 | + "2748", | ||
90 | + "2747", | ||
91 | + "11656", | ||
92 | + "10824", | ||
93 | + "2743", | ||
94 | + "10820", | ||
95 | + "10808", | ||
96 | + "11655", | ||
97 | + "11720", | ||
98 | + "1008", | ||
99 | + "10951", | ||
100 | + "2557", | ||
101 | + "1021", | ||
102 | + "1966", | ||
103 | + "2164", | ||
104 | + "10799", | ||
105 | + "17413", | ||
106 | + "10866", | ||
107 | + "1158", | ||
108 | + "10845", | ||
109 | + "1406", | ||
110 | + "1874", | ||
111 | + "9012", | ||
112 | + "9093", | ||
113 | + "10828", | ||
114 | + "11721", | ||
115 | + "11719", | ||
116 | + "11718", | ||
117 | + "10953", | ||
118 | + "2558", | ||
119 | + "10814", | ||
120 | + "1181", | ||
121 | + "11651", | ||
122 | + "11650", | ||
123 | + "1427", | ||
124 | + "2108", | ||
125 | + "10989", | ||
126 | + "2751", | ||
127 | + "2750", | ||
128 | + "1436", | ||
129 | + "1018", | ||
130 | + "7568", | ||
131 | + "2231", | ||
132 | + "2798", | ||
133 | + "1002", | ||
134 | + "3053", | ||
135 | + "4153", | ||
136 | + "3009", | ||
137 | + "1085", | ||
138 | + "9020", | ||
139 | + "4948", | ||
140 | + "1929", | ||
141 | + "2581", | ||
142 | + "1978", | ||
143 | + "2292", | ||
144 | + "6064", | ||
145 | + "2775", | ||
146 | + "10250", | ||
147 | + "2869", | ||
148 | + "1011", | ||
149 | + "1193", | ||
150 | + "2839", | ||
151 | + "1712", | ||
152 | + "1316", | ||
153 | + "2941", | ||
154 | + "5622", | ||
155 | + "2908", | ||
156 | + "1152", | ||
157 | + "1157", | ||
158 | + "2675", | ||
159 | + "10809", | ||
160 | + "11720", | ||
161 | + "11654", | ||
162 | + "11729", | ||
163 | + "2447", | ||
164 | + "3052", | ||
165 | + "10818", | ||
166 | + "10872", | ||
167 | + "10870", | ||
168 | + "1065", | ||
169 | + "4673", | ||
170 | + "15596", | ||
171 | + "4344", | ||
172 | + "2920", | ||
173 | + "8958", | ||
174 | + "1546", | ||
175 | + "2577", | ||
176 | + "2562", | ||
177 | + "1110", | ||
178 | + "10951", | ||
179 | + "10952", | ||
180 | + "10871", | ||
181 | + "2439", | ||
182 | + "2438", | ||
183 | + "11022", | ||
184 | + "11021", | ||
185 | + "2742", | ||
186 | + "2741", | ||
187 | + "15552", | ||
188 | + "8393", | ||
189 | + "10950", | ||
190 | + "2739", | ||
191 | + "10817", | ||
192 | + "2884", | ||
193 | + "2753", | ||
194 | + "9498", | ||
195 | + "1330", | ||
196 | + "2588", | ||
197 | + "10430", | ||
198 | + "10869", | ||
199 | + "1008", | ||
200 | + "10998", | ||
201 | + "7287", | ||
202 | + "10172", | ||
203 | + "10171", | ||
204 | + "10718", | ||
205 | + "1001", | ||
206 | + "1000", | ||
207 | + "2557", | ||
208 | +]; |
1 | const jwt = require("jsonwebtoken"); | 1 | const jwt = require("jsonwebtoken"); |
2 | const User = require("../models/user"); | 2 | const User = require("../models/user"); |
3 | + | ||
3 | const jwtMiddleware = async (ctx, next) => { | 4 | const jwtMiddleware = async (ctx, next) => { |
4 | const token = ctx.cookies.get("access_token"); | 5 | const token = ctx.cookies.get("access_token"); |
6 | + | ||
5 | if (!token) { | 7 | if (!token) { |
6 | - //토큰이 없을 때 | ||
7 | return next(); | 8 | return next(); |
8 | } | 9 | } |
9 | try { | 10 | try { |
10 | - const decoded = jwt.verify(token, process.env.JWT_TOKEN); | 11 | + const decoded = jwt.verify(token, process.env.JWT_SECRET); |
11 | ctx.state.user = { | 12 | ctx.state.user = { |
12 | _id: decoded._id, | 13 | _id: decoded._id, |
13 | username: decoded.username, | 14 | username: decoded.username, |
14 | }; | 15 | }; |
15 | - //토큰의 남은 유효 기간이 2일 이하라면 재발급 | 16 | + const now = Math.floor(Date.now() / 1000); |
16 | - if (decoded.exp - Date.now() / 1000 < 60 * 60 * 24 * 2) { | 17 | + if (decoded.exp - now < 60 * 60 * 24 * 3.5) { |
17 | const user = await User.findById(decoded._id); | 18 | const user = await User.findById(decoded._id); |
18 | const token = user.generateToken(); | 19 | const token = user.generateToken(); |
19 | ctx.cookies.set("access_token", token, { | 20 | ctx.cookies.set("access_token", token, { |
20 | - maxAge: 1000 * 60 * 60 * 24 * 7, | 21 | + maxAge: 1000 * 60 * 60 * 24 * 7, //7days |
21 | httpOnly: true, | 22 | httpOnly: true, |
22 | }); | 23 | }); |
23 | } | 24 | } |
25 | + | ||
24 | return next(); | 26 | return next(); |
25 | } catch (e) { | 27 | } catch (e) { |
26 | return next(); | 28 | return next(); |
27 | } | 29 | } |
28 | }; | 30 | }; |
29 | - | ||
30 | module.exports = jwtMiddleware; | 31 | module.exports = jwtMiddleware; | ... | ... |
... | @@ -7,6 +7,7 @@ const ProfileSchema = new Schema({ | ... | @@ -7,6 +7,7 @@ const ProfileSchema = new Schema({ |
7 | userBJID: String, | 7 | userBJID: String, |
8 | solvedBJ: Object, | 8 | solvedBJ: Object, |
9 | friendList: [String], | 9 | friendList: [String], |
10 | + slackWebHookURL: String, | ||
10 | }); | 11 | }); |
11 | ProfileSchema.statics.findByUsername = function (username) { | 12 | ProfileSchema.statics.findByUsername = function (username) { |
12 | return this.findOne({ username }); | 13 | return this.findOne({ username }); | ... | ... |
jaksimsamil-server/src/util/analyzeBJ.js
0 → 100644
1 | +/* | ||
2 | +2. 현재 날짜와의 차이 => | ||
3 | +3. 오늘 푼 문제 => 앞에서부터 순회하면서 데이트 같은거 찾기 | ||
4 | +3. 최근 일주일간 푼 문제 수 => 앞에서부터 순회하면서 - 값이 | ||
5 | +4. 추천 문제 => 정규 셋에서 없는거 찾기 | ||
6 | +5. 날짜별로 묶기. | ||
7 | +데이터베이스에서 처리하자 | ||
8 | +*/ | ||
9 | + | ||
10 | +let moment = require('moment'); | ||
11 | + | ||
12 | +exports.analyzeBJ = function (solvedBJ) { | ||
13 | + try { | ||
14 | + if (solvedBJ) { | ||
15 | + console.log(solvedBJ[0]); | ||
16 | + let presentDate = moment(); | ||
17 | + let presentDate_str = presentDate.format('YYYYMMDD'); | ||
18 | + let latestDate = moment(solvedBJ[0].solved_date, 'YYYYMMDD'); | ||
19 | + let difflatest = presentDate.diff(latestDate, 'days'); | ||
20 | + | ||
21 | + let solvedBJbyDATE = {}; | ||
22 | + for (let i = 0; i < solvedBJ.length; i++) { | ||
23 | + if (!(solvedBJ[i].solved_date in solvedBJbyDATE)) { | ||
24 | + solvedBJbyDATE[solvedBJ[i].solved_date] = []; | ||
25 | + solvedBJbyDATE[solvedBJ[i].solved_date].push(solvedBJ[i]); | ||
26 | + } else { | ||
27 | + solvedBJbyDATE[solvedBJ[i].solved_date].push(solvedBJ[i]); | ||
28 | + } | ||
29 | + } | ||
30 | + | ||
31 | + let latestNum = solvedBJbyDATE[solvedBJ[0].solved_date].length; | ||
32 | + let presentNum = | ||
33 | + presentDate_str in solvedBJbyDATE | ||
34 | + ? solvedBJbyDATE[presentDate_str].length | ||
35 | + : 0; | ||
36 | + let returnOBJ = { | ||
37 | + latestDate: latestDate.format('YYYYMMDD'), | ||
38 | + difflatest: difflatest, | ||
39 | + latestNum: latestNum, | ||
40 | + presentNum: presentNum, | ||
41 | + solvedBJbyDATE: solvedBJbyDATE, | ||
42 | + }; | ||
43 | + console.log(returnOBJ); | ||
44 | + return returnOBJ; | ||
45 | + } | ||
46 | + } catch (e) { | ||
47 | + console.log(e); | ||
48 | + } | ||
49 | +}; |
jaksimsamil-server/src/util/compareBJ.js
0 → 100644
1 | +/* | ||
2 | +집중을 해보자. | ||
3 | +새거와 데이터가 있다. | ||
4 | +데이터 기준으로 새거에 자료가 있으면 넘어가기 | ||
5 | +없으면 새 배열에 추가 | ||
6 | +키만 모아둔 리스트를 만들자. | ||
7 | +그렇게 해서 | ||
8 | +반복은 새거 길이만큼 | ||
9 | +데이터에 있으면 추가 X | ||
10 | +없으면 추가 | ||
11 | +그렇게 반환 | ||
12 | +*/ | ||
13 | + | ||
14 | +exports.compareBJ = function (solvedBJ_new, problem_set) { | ||
15 | + try { | ||
16 | + let new_obj = []; | ||
17 | + for (let i = 0; i < solvedBJ.length; i++) { | ||
18 | + if (solvedBJ_new[i].problem_number in problem_set) { | ||
19 | + new_obj.push(solvedBJ_new[i]); | ||
20 | + } | ||
21 | + } | ||
22 | + console.log(new_obj); | ||
23 | + } catch (e) { | ||
24 | + console.log(e); | ||
25 | + } | ||
26 | +}; |
jaksimsamil-server/src/util/sendSlack.js
0 → 100644
1 | +const Slack = require("slack-node"); // 슬랙 모듈 사용 | ||
2 | + | ||
3 | +const webhookUri = | ||
4 | + "https://hooks.slack.com/services/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"; // Webhook URL | ||
5 | + | ||
6 | +const slack = new Slack(); | ||
7 | +slack.setWebhook(webhookUri); | ||
8 | + | ||
9 | +const send = async (message) => { | ||
10 | + slack.webhook( | ||
11 | + { | ||
12 | + text: message, | ||
13 | + }, | ||
14 | + function (err, response) { | ||
15 | + console.log(response); | ||
16 | + } | ||
17 | + ); | ||
18 | +}; | ||
19 | + | ||
20 | +send("hello"); |
jaksimsamil-server/src/util/test.js
0 → 100644
1 | +var getBJ = require("./getBJ"); | ||
2 | +var fs = require("fs"); | ||
3 | + | ||
4 | +let dataset = [ | ||
5 | + "1517", | ||
6 | + "2448", | ||
7 | + "1891", | ||
8 | + "1074", | ||
9 | + "2263", | ||
10 | + "1780", | ||
11 | + "11728", | ||
12 | + "10816", | ||
13 | + "10815", | ||
14 | + "2109", | ||
15 | + "1202", | ||
16 | + "1285", | ||
17 | + "2138", | ||
18 | + "1080", | ||
19 | + "11399", | ||
20 | + "1931", | ||
21 | + "11047", | ||
22 | + "15666", | ||
23 | + "15665", | ||
24 | + "15664", | ||
25 | + "15663", | ||
26 | + "15657", | ||
27 | + "15656", | ||
28 | + "15655", | ||
29 | + "15654", | ||
30 | + "15652", | ||
31 | + "15651", | ||
32 | + "15650", | ||
33 | + "15649", | ||
34 | + "6603", | ||
35 | + "10971", | ||
36 | + "10819", | ||
37 | + "10973", | ||
38 | + "10974", | ||
39 | + "10972", | ||
40 | + "7576", | ||
41 | + "1248", | ||
42 | + "2529", | ||
43 | + "15661", | ||
44 | + "14501", | ||
45 | + "1759", | ||
46 | + "14391", | ||
47 | + "14889", | ||
48 | + "1182", | ||
49 | + "11723", | ||
50 | + "1748", | ||
51 | + "6064", | ||
52 | + "1107", | ||
53 | + "3085", | ||
54 | + "2309", | ||
55 | + "1748", | ||
56 | + "14500", | ||
57 | + "1107", | ||
58 | + "1476", | ||
59 | + "3085", | ||
60 | + "2309", | ||
61 | + "1261", | ||
62 | + "13549", | ||
63 | + "14226", | ||
64 | + "13913", | ||
65 | + "1697", | ||
66 | + "1967", | ||
67 | + "1167", | ||
68 | + "11725", | ||
69 | + "2250", | ||
70 | + "1991", | ||
71 | + "7562", | ||
72 | + "2178", | ||
73 | + "4963", | ||
74 | + "2667", | ||
75 | + "1707", | ||
76 | + "11724", | ||
77 | + "1260", | ||
78 | + "13023", | ||
79 | + "11652", | ||
80 | + "1377", | ||
81 | + "11004", | ||
82 | + "10825", | ||
83 | + "2751", | ||
84 | + "9461", | ||
85 | + "1699", | ||
86 | + "9095", | ||
87 | + "2225", | ||
88 | + "2133", | ||
89 | + "11727", | ||
90 | + "11726", | ||
91 | + "1463", | ||
92 | + "2748", | ||
93 | + "2747", | ||
94 | + "11656", | ||
95 | + "10824", | ||
96 | + "2743", | ||
97 | + "10820", | ||
98 | + "10808", | ||
99 | + "11655", | ||
100 | + "11720", | ||
101 | + "1008", | ||
102 | + "10951", | ||
103 | + "2557", | ||
104 | + "1021", | ||
105 | + "1966", | ||
106 | + "2164", | ||
107 | + "10799", | ||
108 | + "17413", | ||
109 | + "10866", | ||
110 | + "1158", | ||
111 | + "10845", | ||
112 | + "1406", | ||
113 | + "1874", | ||
114 | + "9012", | ||
115 | + "9093", | ||
116 | + "10828", | ||
117 | + "11721", | ||
118 | + "11719", | ||
119 | + "11718", | ||
120 | + "10953", | ||
121 | + "2558", | ||
122 | + "10814", | ||
123 | + "1181", | ||
124 | + "11651", | ||
125 | + "11650", | ||
126 | + "1427", | ||
127 | + "2108", | ||
128 | + "10989", | ||
129 | + "2751", | ||
130 | + "2750", | ||
131 | + "1436", | ||
132 | + "1018", | ||
133 | + "7568", | ||
134 | + "2231", | ||
135 | + "2798", | ||
136 | + "1002", | ||
137 | + "3053", | ||
138 | + "4153", | ||
139 | + "3009", | ||
140 | + "1085", | ||
141 | + "9020", | ||
142 | + "4948", | ||
143 | + "1929", | ||
144 | + "2581", | ||
145 | + "1978", | ||
146 | + "2292", | ||
147 | + "6064", | ||
148 | + "2775", | ||
149 | + "10250", | ||
150 | + "2869", | ||
151 | + "1011", | ||
152 | + "1193", | ||
153 | + "2839", | ||
154 | + "1712", | ||
155 | + "1316", | ||
156 | + "2941", | ||
157 | + "5622", | ||
158 | + "2908", | ||
159 | + "1152", | ||
160 | + "1157", | ||
161 | + "2675", | ||
162 | + "10809", | ||
163 | + "11720", | ||
164 | + "11654", | ||
165 | + "11729", | ||
166 | + "2447", | ||
167 | + "3052", | ||
168 | + "10818", | ||
169 | + "10872", | ||
170 | + "10870", | ||
171 | + "1065", | ||
172 | + "4673", | ||
173 | + "15596", | ||
174 | + "4344", | ||
175 | + "2920", | ||
176 | + "8958", | ||
177 | + "1546", | ||
178 | + "2577", | ||
179 | + "2562", | ||
180 | + "1110", | ||
181 | + "10951", | ||
182 | + "10952", | ||
183 | + "10871", | ||
184 | + "2439", | ||
185 | + "2438", | ||
186 | + "11022", | ||
187 | + "11021", | ||
188 | + "2742", | ||
189 | + "2741", | ||
190 | + "15552", | ||
191 | + "8393", | ||
192 | + "10950", | ||
193 | + "2739", | ||
194 | + "10817", | ||
195 | + "2884", | ||
196 | + "2753", | ||
197 | + "9498", | ||
198 | + "1330", | ||
199 | + "2588", | ||
200 | + "10430", | ||
201 | + "10869", | ||
202 | + "1008", | ||
203 | + "10998", | ||
204 | + "7287", | ||
205 | + "10172", | ||
206 | + "10171", | ||
207 | + "10718", | ||
208 | + "1001", | ||
209 | + "1000", | ||
210 | + "2557", | ||
211 | +]; | ||
212 | + | ||
213 | +const test = async (userid) => { | ||
214 | + let lst = await getBJ.getBJ(userid); | ||
215 | + let return_lst = []; | ||
216 | + for (let i = 0; i < lst.length; i++) { | ||
217 | + return_lst.push(lst[i].problem_number); | ||
218 | + } | ||
219 | + | ||
220 | + var stringJson = JSON.stringify(return_lst) + "\n"; | ||
221 | + fs.open("test.json", "a", "666", function (err, id) { | ||
222 | + if (err) { | ||
223 | + console.log("file open err!!"); | ||
224 | + } else { | ||
225 | + fs.write(id, stringJson, null, "utf8", function (err) { | ||
226 | + console.log("file was saved!"); | ||
227 | + }); | ||
228 | + } | ||
229 | + }); | ||
230 | +}; | ||
231 | + | ||
232 | +/* | ||
233 | + | ||
234 | +*/ | ||
235 | +test("jwseo001"); |
jaksimsamil-server/src/util/test.json
0 → 100644
1 | +[ | ||
2 | + "1517", | ||
3 | + "2448", | ||
4 | + "1891", | ||
5 | + "1074", | ||
6 | + "2263", | ||
7 | + "1780", | ||
8 | + "11728", | ||
9 | + "10816", | ||
10 | + "10815", | ||
11 | + "2109", | ||
12 | + "1202", | ||
13 | + "1285", | ||
14 | + "2138", | ||
15 | + "1080", | ||
16 | + "11399", | ||
17 | + "1931", | ||
18 | + "11047", | ||
19 | + "15666", | ||
20 | + "15665", | ||
21 | + "15664", | ||
22 | + "15663", | ||
23 | + "15657", | ||
24 | + "15656", | ||
25 | + "15655", | ||
26 | + "15654", | ||
27 | + "15652", | ||
28 | + "15651", | ||
29 | + "15650", | ||
30 | + "15649", | ||
31 | + "6603", | ||
32 | + "10971", | ||
33 | + "10819", | ||
34 | + "10973", | ||
35 | + "10974", | ||
36 | + "10972", | ||
37 | + "7576", | ||
38 | + "1248", | ||
39 | + "2529", | ||
40 | + "15661", | ||
41 | + "14501", | ||
42 | + "1759", | ||
43 | + "14391", | ||
44 | + "14889", | ||
45 | + "1182", | ||
46 | + "11723", | ||
47 | + "1748", | ||
48 | + "6064", | ||
49 | + "1107", | ||
50 | + "3085", | ||
51 | + "2309", | ||
52 | + "1748", | ||
53 | + "14500", | ||
54 | + "1107", | ||
55 | + "1476", | ||
56 | + "3085", | ||
57 | + "2309", | ||
58 | + "1261", | ||
59 | + "13549", | ||
60 | + "14226", | ||
61 | + "13913", | ||
62 | + "1697", | ||
63 | + "1967", | ||
64 | + "1167", | ||
65 | + "11725", | ||
66 | + "2250", | ||
67 | + "1991", | ||
68 | + "7562", | ||
69 | + "2178", | ||
70 | + "4963", | ||
71 | + "2667", | ||
72 | + "1707", | ||
73 | + "11724", | ||
74 | + "1260", | ||
75 | + "13023", | ||
76 | + "11652", | ||
77 | + "1377", | ||
78 | + "11004", | ||
79 | + "10825", | ||
80 | + "2751", | ||
81 | + "9461", | ||
82 | + "1699", | ||
83 | + "9095", | ||
84 | + "2225", | ||
85 | + "2133", | ||
86 | + "11727", | ||
87 | + "11726", | ||
88 | + "1463", | ||
89 | + "2748", | ||
90 | + "2747", | ||
91 | + "11656", | ||
92 | + "10824", | ||
93 | + "2743", | ||
94 | + "10820", | ||
95 | + "10808", | ||
96 | + "11655", | ||
97 | + "11720", | ||
98 | + "1008", | ||
99 | + "10951", | ||
100 | + "2557", | ||
101 | + "1021", | ||
102 | + "1966", | ||
103 | + "2164", | ||
104 | + "10799", | ||
105 | + "17413", | ||
106 | + "10866", | ||
107 | + "1158", | ||
108 | + "10845", | ||
109 | + "1406", | ||
110 | + "1874", | ||
111 | + "9012", | ||
112 | + "9093", | ||
113 | + "10828", | ||
114 | + "11721", | ||
115 | + "11719", | ||
116 | + "11718", | ||
117 | + "10953", | ||
118 | + "2558", | ||
119 | + "10814", | ||
120 | + "1181", | ||
121 | + "11651", | ||
122 | + "11650", | ||
123 | + "1427", | ||
124 | + "2108", | ||
125 | + "10989", | ||
126 | + "2751", | ||
127 | + "2750", | ||
128 | + "1436", | ||
129 | + "1018", | ||
130 | + "7568", | ||
131 | + "2231", | ||
132 | + "2798", | ||
133 | + "1002", | ||
134 | + "3053", | ||
135 | + "4153", | ||
136 | + "3009", | ||
137 | + "1085", | ||
138 | + "9020", | ||
139 | + "4948", | ||
140 | + "1929", | ||
141 | + "2581", | ||
142 | + "1978", | ||
143 | + "2292", | ||
144 | + "6064", | ||
145 | + "2775", | ||
146 | + "10250", | ||
147 | + "2869", | ||
148 | + "1011", | ||
149 | + "1193", | ||
150 | + "2839", | ||
151 | + "1712", | ||
152 | + "1316", | ||
153 | + "2941", | ||
154 | + "5622", | ||
155 | + "2908", | ||
156 | + "1152", | ||
157 | + "1157", | ||
158 | + "2675", | ||
159 | + "10809", | ||
160 | + "11720", | ||
161 | + "11654", | ||
162 | + "11729", | ||
163 | + "2447", | ||
164 | + "3052", | ||
165 | + "10818", | ||
166 | + "10872", | ||
167 | + "10870", | ||
168 | + "1065", | ||
169 | + "4673", | ||
170 | + "15596", | ||
171 | + "4344", | ||
172 | + "2920", | ||
173 | + "8958", | ||
174 | + "1546", | ||
175 | + "2577", | ||
176 | + "2562", | ||
177 | + "1110", | ||
178 | + "10951", | ||
179 | + "10952", | ||
180 | + "10871", | ||
181 | + "2439", | ||
182 | + "2438", | ||
183 | + "11022", | ||
184 | + "11021", | ||
185 | + "2742", | ||
186 | + "2741", | ||
187 | + "15552", | ||
188 | + "8393", | ||
189 | + "10950", | ||
190 | + "2739", | ||
191 | + "10817", | ||
192 | + "2884", | ||
193 | + "2753", | ||
194 | + "9498", | ||
195 | + "1330", | ||
196 | + "2588", | ||
197 | + "10430", | ||
198 | + "10869", | ||
199 | + "1008", | ||
200 | + "10998", | ||
201 | + "7287", | ||
202 | + "10172", | ||
203 | + "10171", | ||
204 | + "10718", | ||
205 | + "1001", | ||
206 | + "1000", | ||
207 | + "2557" | ||
208 | +] |
-
Please register or login to post a comment