Showing
49 changed files
with
1547 additions
and
61 deletions
jaksimsamil-page/package-lock.json
0 → 100644
This diff could not be displayed because it is too large.
... | @@ -3,13 +3,24 @@ | ... | @@ -3,13 +3,24 @@ |
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.1", | ||
12 | + "include-media": "^1.4.9", | ||
13 | + "open-color": "^1.7.0", | ||
9 | "react": "^16.13.1", | 14 | "react": "^16.13.1", |
10 | "react-dom": "^16.13.1", | 15 | "react-dom": "^16.13.1", |
16 | + "react-redux": "^7.2.0", | ||
11 | "react-router-dom": "^5.2.0", | 17 | "react-router-dom": "^5.2.0", |
12 | - "react-scripts": "3.4.1" | 18 | + "react-scripts": "3.4.1", |
19 | + "redux": "^4.0.5", | ||
20 | + "redux-actions": "^2.6.5", | ||
21 | + "redux-devtools-extension": "^2.13.8", | ||
22 | + "redux-saga": "^1.1.3", | ||
23 | + "styled-components": "^5.1.1" | ||
13 | }, | 24 | }, |
14 | "scripts": { | 25 | "scripts": { |
15 | "start": "react-scripts start", | 26 | "start": "react-scripts start", |
... | @@ -31,5 +42,6 @@ | ... | @@ -31,5 +42,6 @@ |
31 | "last 1 firefox version", | 42 | "last 1 firefox version", |
32 | "last 1 safari version" | 43 | "last 1 safari version" |
33 | ] | 44 | ] |
34 | - } | 45 | + }, |
46 | + "proxy": "http://localhost:4000" | ||
35 | } | 47 | } | ... | ... |
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 styled from 'styled-components'; | ||
4 | +import palette from '../../lib/styles/palette'; | ||
5 | +import Button from '@material-ui/core/Button'; | ||
6 | +import TextField from '@material-ui/core/TextField'; | ||
7 | + | ||
8 | +const useStyles = makeStyles((theme) => ({ | ||
9 | + root: { | ||
10 | + '& > *': { | ||
11 | + margin: theme.spacing(1), | ||
12 | + }, | ||
13 | + }, | ||
14 | +})); | ||
15 | + | ||
16 | +const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => { | ||
17 | + const classes = useStyles(); | ||
18 | + return ( | ||
19 | + <div> | ||
20 | + <form onSubmit={onBJIDSubmit}> | ||
21 | + <TextField | ||
22 | + name="userBJID" | ||
23 | + onChange={onChange} | ||
24 | + value={profile.userBJID} | ||
25 | + placeholder="백준 아이디" | ||
26 | + label="백준 아이디" | ||
27 | + /> | ||
28 | + <Button variant="outlined" type="submit"> | ||
29 | + 등록 | ||
30 | + </Button> | ||
31 | + </form> | ||
32 | + <Button variant="outlined" onClick={onSyncBJIDSubmit}> | ||
33 | + 동기화 | ||
34 | + </Button> | ||
35 | + </div> | ||
36 | + ); | ||
37 | +}; | ||
38 | +export default BJIDForm; |
1 | +import React from 'react'; | ||
2 | +import styled from 'styled-components'; | ||
3 | +import Button from '../common/Button'; | ||
4 | +import palette from '../../lib/styles/palette'; | ||
5 | +import BJIDForm from './BJIDForm'; | ||
6 | +import { makeStyles } from '@material-ui/core/styles'; | ||
7 | +import Paper from '@material-ui/core/Paper'; | ||
8 | +import Grid from '@material-ui/core/Grid'; | ||
9 | + | ||
10 | +const SettingFormBlock = styled.div` | ||
11 | + h3 { | ||
12 | + margin: 0; | ||
13 | + color: ${palette.gray[8]}; | ||
14 | + margin-bottom: 1rem; | ||
15 | + } | ||
16 | + background: ${palette.gray[2]}; | ||
17 | + margin: 0 auto; | ||
18 | + display: flex; | ||
19 | + flex-direction: column; | ||
20 | +`; | ||
21 | +const StyledInput = styled.input` | ||
22 | + font-size: 1rem; | ||
23 | + border: none; | ||
24 | + border-bottom: 1px solid ${palette.gray[5]}; | ||
25 | + padding-bottom: 0.5rem; | ||
26 | + outline: none; | ||
27 | + &:focus { | ||
28 | + color: $oc-teal-7; | ||
29 | + border-bottom: 1px solid ${palette.gray[7]}; | ||
30 | + } | ||
31 | + & + & { | ||
32 | + margin-top: 1rem; | ||
33 | + } | ||
34 | +`; | ||
35 | +const SectionContainer = styled.div` | ||
36 | + display: flex; | ||
37 | +`; | ||
38 | + | ||
39 | +const useStyles = makeStyles((theme) => ({ | ||
40 | + root: { | ||
41 | + flexGrow: 1, | ||
42 | + background: palette.gray[2], | ||
43 | + }, | ||
44 | + paper: { | ||
45 | + margin: 'auto', | ||
46 | + textAlign: 'center', | ||
47 | + padding: 30, | ||
48 | + }, | ||
49 | +})); | ||
50 | + | ||
51 | +const SettingForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => { | ||
52 | + const classes = useStyles(); | ||
53 | + return ( | ||
54 | + <div className={classes.root}> | ||
55 | + <Grid container spacing={3}> | ||
56 | + <Grid item xs={12}> | ||
57 | + <Paper className={classes.paper}> | ||
58 | + <h3>{profile.username}</h3> | ||
59 | + </Paper> | ||
60 | + </Grid> | ||
61 | + <Grid container item xs={12}> | ||
62 | + <Paper className={classes.paper} elevation={3}> | ||
63 | + <BJIDForm | ||
64 | + profile={profile} | ||
65 | + onChange={onChange} | ||
66 | + onBJIDSubmit={onBJIDSubmit} | ||
67 | + onSyncBJIDSubmit={onSyncBJIDSubmit} | ||
68 | + /> | ||
69 | + </Paper> | ||
70 | + </Grid> | ||
71 | + </Grid> | ||
72 | + </div> | ||
73 | + ); | ||
74 | +}; | ||
75 | + | ||
76 | +export default SettingForm; |
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, useState } 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 [isLogin, setLogin] = useState(false); | ||
10 | + const { user, profile } = useSelector(({ user, profile }) => ({ | ||
11 | + user: user.user, | ||
12 | + profile: profile, | ||
13 | + })); | ||
14 | + useEffect(() => { | ||
15 | + analyzeBJ(profile.solvedBJ); | ||
16 | + }, [profile.solvedBJ]); | ||
17 | + useEffect(() => { | ||
18 | + setLogin(true); | ||
19 | + if (user) { | ||
20 | + let username = user.username; | ||
21 | + dispatch(getPROFILE({ username })); | ||
22 | + } | ||
23 | + }, [dispatch, user]); | ||
24 | + return <HomeForm />; | ||
25 | +}; | ||
26 | +export default withRouter(HomeContainer); |
1 | +import React, { useEffect, useState } 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 | +} from '../../modules/profile'; | ||
11 | +import SettingForm from '../../components/setting/SettingForm'; | ||
12 | + | ||
13 | +const SettingContainer = ({ history }) => { | ||
14 | + const dispatch = useDispatch(); | ||
15 | + const { user, profile } = useSelector(({ user, profile }) => ({ | ||
16 | + user: user.user, | ||
17 | + profile: profile, | ||
18 | + })); | ||
19 | + | ||
20 | + const onChange = (e) => { | ||
21 | + const { value, name } = e.target; | ||
22 | + dispatch( | ||
23 | + changeField({ | ||
24 | + key: name, | ||
25 | + value: value, | ||
26 | + }), | ||
27 | + ); | ||
28 | + }; | ||
29 | + | ||
30 | + const onSyncBJIDSubmit = (e) => { | ||
31 | + e.preventDefault(); | ||
32 | + let username = profile.username; | ||
33 | + dispatch(syncBJID({ username })); | ||
34 | + }; | ||
35 | + | ||
36 | + const onBJIDSubmit = (e) => { | ||
37 | + e.preventDefault(); | ||
38 | + let username = profile.username; | ||
39 | + let userBJID = profile.userBJID; | ||
40 | + | ||
41 | + dispatch(setBJID({ username, userBJID })); | ||
42 | + }; | ||
43 | + | ||
44 | + useEffect(() => { | ||
45 | + if (!user) { | ||
46 | + alert('로그인이 필요합니다 '); | ||
47 | + history.push('/'); | ||
48 | + } else { | ||
49 | + let username = user.username; | ||
50 | + dispatch(getPROFILE({ username })); | ||
51 | + return () => { | ||
52 | + dispatch(initializeProfile()); | ||
53 | + }; | ||
54 | + } | ||
55 | + }, [dispatch, user, history]); | ||
56 | + | ||
57 | + return ( | ||
58 | + <SettingForm | ||
59 | + type="setting" | ||
60 | + onChange={onChange} | ||
61 | + onBJIDSubmit={onBJIDSubmit} | ||
62 | + onSyncBJIDSubmit={onSyncBJIDSubmit} | ||
63 | + profile={profile} | ||
64 | + ></SettingForm> | ||
65 | + ); | ||
66 | +}; | ||
67 | + | ||
68 | +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 | + | ||
9 | +export const getPROFILE = ({ username }) => | ||
10 | + client.post('api/profile/getprofile', { username }); | ||
11 | + | ||
12 | +export const syncBJ = ({ username }) => | ||
13 | + 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/lib/util/analyzeBJ.js
0 → 100644
1 | +/* | ||
2 | +1. 날짜 순 정렬 | ||
3 | +2. 현재 날짜와의 차이 | ||
4 | +3. 최근 일주일간 푼 문제 수 | ||
5 | +4. 추천 문제 | ||
6 | +*/ | ||
7 | +exports.analyzeBJ = function (solvedBJ) { | ||
8 | + console.log(typeof solvedBJ); | ||
9 | + if (solvedBJ) { | ||
10 | + solvedBJ.sort(function (a, b) { | ||
11 | + return a.solvedDate > b.solvedDate | ||
12 | + ? -1 | ||
13 | + : a.solvedDate < b.solvedDate | ||
14 | + ? 1 | ||
15 | + : 0; | ||
16 | + }); | ||
17 | + console.log(solvedBJ); | ||
18 | + } | ||
19 | +}; |
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 | + GET_PROFILE, | ||
16 | + GET_PROFILE_SUCCESS, | ||
17 | + GET_PROFILE_FAILURE, | ||
18 | +] = createRequestActionTypes('profile/GET_PROFILE'); | ||
19 | + | ||
20 | +const [ | ||
21 | + SYNC_BJID, | ||
22 | + SYNC_BJID_SUCCESS, | ||
23 | + SYNC_BJID_FAILURE, | ||
24 | +] = createRequestActionTypes('profile/SYNC_BJID'); | ||
25 | +export const initializeProfile = createAction(INITIALIZE); | ||
26 | +export const syncBJID = createAction(SYNC_BJID, ({ username }) => ({ | ||
27 | + username, | ||
28 | +})); | ||
29 | +export const setBJID = createAction(SET_BJID, ({ username, userBJID }) => ({ | ||
30 | + username, | ||
31 | + userBJID, | ||
32 | +})); | ||
33 | + | ||
34 | +export const changeField = createAction(CHANGE_FIELD, ({ key, value }) => ({ | ||
35 | + key, | ||
36 | + value, | ||
37 | +})); | ||
38 | + | ||
39 | +export const getPROFILE = createAction(GET_PROFILE, ({ username }) => ({ | ||
40 | + username, | ||
41 | +})); | ||
42 | +const initialState = { | ||
43 | + username: '', | ||
44 | + userBJID: '', | ||
45 | + solvedBJ: '', | ||
46 | + friendList: [], | ||
47 | + profileError: '', | ||
48 | +}; | ||
49 | +const getPROFILESaga = createRequestSaga(GET_PROFILE, profileAPI.getPROFILE); | ||
50 | +const setBJIDSaga = createRequestSaga(SET_BJID, profileAPI.setBJID); | ||
51 | +const syncBJIDSaga = createRequestSaga(SYNC_BJID, profileAPI.syncBJ); | ||
52 | +export function* profileSaga() { | ||
53 | + yield takeLatest(SET_BJID, setBJIDSaga); | ||
54 | + yield takeLatest(GET_PROFILE, getPROFILESaga); | ||
55 | + yield takeLatest(SYNC_BJID, syncBJIDSaga); | ||
56 | +} | ||
57 | + | ||
58 | +export default handleActions( | ||
59 | + { | ||
60 | + [INITIALIZE]: (state) => initialState, | ||
61 | + [CHANGE_FIELD]: (state, { payload: { key, value } }) => | ||
62 | + produce(state, (draft) => { | ||
63 | + draft[key] = value; | ||
64 | + }), | ||
65 | + [GET_PROFILE_SUCCESS]: ( | ||
66 | + state, | ||
67 | + { payload: { username, userBJID, solvedBJ, friendList } }, | ||
68 | + ) => ({ | ||
69 | + ...state, | ||
70 | + username: username, | ||
71 | + userBJID: userBJID, | ||
72 | + solvedBJ: solvedBJ, | ||
73 | + friendList: friendList, | ||
74 | + profileError: null, | ||
75 | + }), | ||
76 | + [GET_PROFILE_FAILURE]: (state, { payload: error }) => ({ | ||
77 | + ...state, | ||
78 | + profileError: error, | ||
79 | + }), | ||
80 | + | ||
81 | + [SET_BJID_SUCCESS]: (state, { payload: { userBJID } }) => ({ | ||
82 | + ...state, | ||
83 | + userBJID: userBJID, | ||
84 | + profileError: null, | ||
85 | + }), | ||
86 | + [SET_BJID_FAILURE]: (state, { payload: error }) => ({ | ||
87 | + ...state, | ||
88 | + profileError: error, | ||
89 | + }), | ||
90 | + [SYNC_BJID_SUCCESS]: (state, { payload: { solvedBJ } }) => ({ | ||
91 | + ...state, | ||
92 | + solvedBJ, | ||
93 | + profileError: null, | ||
94 | + }), | ||
95 | + [SYNC_BJID_FAILURE]: (state, { payload: error }) => ({ | ||
96 | + ...state, | ||
97 | + profileError: error, | ||
98 | + }), | ||
99 | + }, | ||
100 | + initialState, | ||
101 | +); |
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 SettingForm from '../components/setting/SettingForm'; | ||
4 | +import SettingContainer from '../containers/setting/SettingContainer'; | ||
5 | + | ||
6 | +const SettingPage = () => { | ||
7 | + return ( | ||
8 | + <div> | ||
9 | + <HeaderContainer /> | ||
10 | + <SettingContainer></SettingContainer> | ||
11 | + </div> | ||
12 | + ); | ||
13 | +}; | ||
14 | + | ||
15 | +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/solved:id | 바로가기 | None | | 29 | +| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | 바로가기 | None | |
30 | -| profile | 유저가 푼 문제 동기화 | Update | api/profile/solved:id | 바로가기 | None | | 30 | +| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | 바로가기 | None | |
31 | -| profile | 유저가 푼 문제 개수 조회 | GET | api/profile/solvednum:id | 바로가기 | None | | 31 | +| profile | 유저 정보 수정 | POST | api/profile/setprofile | 바로가기 | JWT TOKEN | |
32 | -| profile | 추천 문제 조회 | GET | api/profile/recommendps: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 | | ... | ... |
... | @@ -2,6 +2,8 @@ const Koa = require("koa"); | ... | @@ -2,6 +2,8 @@ const Koa = require("koa"); |
2 | const Router = require("koa-router"); | 2 | const Router = require("koa-router"); |
3 | const bodyParser = require("koa-bodyparser"); | 3 | const bodyParser = require("koa-bodyparser"); |
4 | const mongoose = require("mongoose"); | 4 | const mongoose = require("mongoose"); |
5 | +const fs = require("fs"); | ||
6 | +const morgan = require("koa-morgan"); | ||
5 | const jwtMiddleware = require("./src/lib/jwtMiddleware"); | 7 | const jwtMiddleware = require("./src/lib/jwtMiddleware"); |
6 | const api = require("./src/api"); | 8 | const api = require("./src/api"); |
7 | 9 | ||
... | @@ -9,10 +11,13 @@ require("dotenv").config(); | ... | @@ -9,10 +11,13 @@ require("dotenv").config(); |
9 | 11 | ||
10 | const app = new Koa(); | 12 | const app = new Koa(); |
11 | const router = new Router(); | 13 | const router = new Router(); |
12 | - | 14 | +const accessLogStream = fs.createWriteStream(__dirname + "/access.log", { |
15 | + flags: "a", | ||
16 | +}); | ||
17 | +require("dotenv").config(); | ||
13 | app.use(bodyParser()); | 18 | app.use(bodyParser()); |
14 | app.use(jwtMiddleware); | 19 | app.use(jwtMiddleware); |
15 | - | 20 | +app.use(morgan("combined", { stream: accessLogStream })); |
16 | const { SERVER_PORT, MONGO_URL } = process.env; | 21 | const { SERVER_PORT, MONGO_URL } = process.env; |
17 | 22 | ||
18 | router.use("/api", api.routes()); | 23 | router.use("/api", api.routes()); | ... | ... |
This diff is collapsed. Click to expand it.
... | @@ -17,10 +17,14 @@ | ... | @@ -17,10 +17,14 @@ |
17 | "jsonwebtoken": "^8.5.1", | 17 | "jsonwebtoken": "^8.5.1", |
18 | "koa": "^2.12.0", | 18 | "koa": "^2.12.0", |
19 | "koa-bodyparser": "^4.3.0", | 19 | "koa-bodyparser": "^4.3.0", |
20 | + "koa-morgan": "^1.0.1", | ||
20 | "koa-router": "^9.0.1", | 21 | "koa-router": "^9.0.1", |
21 | "mongoose": "^5.9.17", | 22 | "mongoose": "^5.9.17", |
22 | "morgan": "^1.10.0", | 23 | "morgan": "^1.10.0", |
24 | + "node-schedule": "^1.3.2", | ||
23 | "path": "^0.12.7", | 25 | "path": "^0.12.7", |
26 | + "slack-client": "^2.0.6", | ||
27 | + "slack-node": "^0.1.8", | ||
24 | "voca": "^1.4.0" | 28 | "voca": "^1.4.0" |
25 | }, | 29 | }, |
26 | "devDependencies": { | 30 | "devDependencies": { | ... | ... |
1 | const Joi = require("joi"); | 1 | const Joi = require("joi"); |
2 | const User = require("../../models/user"); | 2 | const User = require("../../models/user"); |
3 | +const Profile = require("../../models/profile"); | ||
3 | /* | 4 | /* |
4 | POST /api/auth/register | 5 | POST /api/auth/register |
5 | { | 6 | { |
... | @@ -27,15 +28,19 @@ exports.register = async (ctx) => { | ... | @@ -27,15 +28,19 @@ exports.register = async (ctx) => { |
27 | ctx.status = 409; | 28 | ctx.status = 409; |
28 | return; | 29 | return; |
29 | } | 30 | } |
31 | + const profile = new Profile({ | ||
32 | + username, | ||
33 | + }); | ||
30 | const user = new User({ | 34 | const user = new User({ |
31 | username, | 35 | username, |
32 | }); | 36 | }); |
33 | await user.setPassword(password); | 37 | await user.setPassword(password); |
38 | + await profile.save(); | ||
34 | await user.save(); | 39 | await user.save(); |
35 | ctx.body = user.serialize(); | 40 | ctx.body = user.serialize(); |
36 | 41 | ||
37 | const token = user.generateToken(); | 42 | const token = user.generateToken(); |
38 | - ctx.cookies.set("acces_token", token, { | 43 | + ctx.cookies.set("access_token", token, { |
39 | //3일동안 유효 | 44 | //3일동안 유효 |
40 | maxAge: 1000 * 60 * 60 * 24 * 3, | 45 | maxAge: 1000 * 60 * 60 * 24 * 3, |
41 | httpOnly: true, | 46 | httpOnly: true, |
... | @@ -70,7 +75,7 @@ exports.login = async (ctx) => { | ... | @@ -70,7 +75,7 @@ exports.login = async (ctx) => { |
70 | } | 75 | } |
71 | ctx.body = user.serialize(); | 76 | ctx.body = user.serialize(); |
72 | const token = user.generateToken(); | 77 | const token = user.generateToken(); |
73 | - ctx.cookies.set("acces_token", token, { | 78 | + ctx.cookies.set("access_token", token, { |
74 | //7일동안 유효 | 79 | //7일동안 유효 |
75 | maxAge: 1000 * 60 * 60 * 24 * 7, | 80 | maxAge: 1000 * 60 * 60 * 24 * 7, |
76 | 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; | ... | ... |
1 | const Router = require("koa-router"); | 1 | const Router = require("koa-router"); |
2 | const profile = new Router(); | 2 | const profile = new Router(); |
3 | +const profileCtrl = require("./profile.ctrl"); | ||
3 | 4 | ||
4 | profile.post("/solved:id"); | 5 | profile.post("/solved:id"); |
5 | profile.get("/solvednum:id"); | 6 | profile.get("/solvednum:id"); |
6 | -profile.get("recommendps:id"); | 7 | +profile.get("/recommendps:id"); |
7 | - | 8 | +profile.patch("/syncBJ", profileCtrl.syncBJ); |
9 | +profile.post("/setprofile", profileCtrl.setProfile); | ||
10 | +profile.post("/getprofile", profileCtrl.getProfile); | ||
8 | module.exports = profile; | 11 | module.exports = profile; | ... | ... |
1 | +const Profile = require("../../models/profile"); | ||
2 | +const mongoose = require("mongoose"); | ||
3 | +const getBJ = require("../../util/getBJ"); | ||
4 | +const Joi = require("joi"); | ||
5 | + | ||
6 | +const { ObjectId } = mongoose.Types; | ||
7 | + | ||
8 | +exports.checkObjectId = (ctx, next) => { | ||
9 | + const { username } = ctx.request.body; | ||
10 | + if (!ObjectId.isValid(username)) { | ||
11 | + ctx.status = 400; | ||
12 | + return; | ||
13 | + } | ||
14 | + return next(); | ||
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 | +}; | ||
34 | +/* | ||
35 | +POST /api/proflie/setprofile | ||
36 | +{ | ||
37 | + username: "username", | ||
38 | + userBJID: "userBJID", | ||
39 | + friendList: [String], | ||
40 | +} | ||
41 | + */ | ||
42 | +exports.setProfile = async (ctx) => { | ||
43 | + const schema = Joi.object() | ||
44 | + .keys({ | ||
45 | + username: Joi.string(), | ||
46 | + userBJID: Joi.string(), | ||
47 | + //freindList: Joi.array().items(Joi.string()), | ||
48 | + }) | ||
49 | + .unknown(); | ||
50 | + | ||
51 | + const result = Joi.validate(ctx.request.body, schema); | ||
52 | + if (result.error) { | ||
53 | + ctx.status = 400; | ||
54 | + ctx.body = result.error; | ||
55 | + return; | ||
56 | + } | ||
57 | + | ||
58 | + try { | ||
59 | + const profile = await Profile.findOneAndUpdate( | ||
60 | + { username: ctx.request.body.username }, | ||
61 | + ctx.request.body, | ||
62 | + { | ||
63 | + new: true, | ||
64 | + } | ||
65 | + ).exec(); | ||
66 | + | ||
67 | + if (!profile) { | ||
68 | + ctx.status = 404; | ||
69 | + return; | ||
70 | + } | ||
71 | + ctx.body = profile; | ||
72 | + } catch (e) { | ||
73 | + ctx.throw(500, e); | ||
74 | + } | ||
75 | +}; | ||
76 | +/* | ||
77 | +PATCH /api/proflie/syncBJ | ||
78 | +{ | ||
79 | + username: 'userid' | ||
80 | +} | ||
81 | + */ | ||
82 | +exports.syncBJ = async function (ctx) { | ||
83 | + const { username } = ctx.request.body; | ||
84 | + | ||
85 | + if (!username) { | ||
86 | + ctx.status = 401; | ||
87 | + return; | ||
88 | + } | ||
89 | + | ||
90 | + try { | ||
91 | + const profile = await Profile.findByUsername(username); | ||
92 | + if (!profile) { | ||
93 | + ctx.status = 401; | ||
94 | + return; | ||
95 | + } | ||
96 | + const BJID = await profile.getBJID(); | ||
97 | + let BJdata = await getBJ.getBJ(BJID); | ||
98 | + const updateprofile = await Profile.findOneAndUpdate( | ||
99 | + { username: username }, | ||
100 | + { solvedBJ: BJdata }, | ||
101 | + { new: true } | ||
102 | + ).exec(); | ||
103 | + ctx.body = updateprofile; | ||
104 | + } catch (e) { | ||
105 | + ctx.throw(500, e); | ||
106 | + } | ||
107 | +}; |
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; | ... | ... |
jaksimsamil-server/src/models/profile.js
0 → 100644
1 | +const mongoose = require("mongoose"); | ||
2 | + | ||
3 | +const { Schema } = mongoose; | ||
4 | + | ||
5 | +const ProfileSchema = new Schema({ | ||
6 | + username: { type: String, required: true, unique: true }, | ||
7 | + userBJID: String, | ||
8 | + solvedBJ: Object, | ||
9 | + friendList: [String], | ||
10 | +}); | ||
11 | +ProfileSchema.statics.findByUsername = function (username) { | ||
12 | + return this.findOne({ username }); | ||
13 | +}; | ||
14 | +ProfileSchema.methods.getBJID = function () { | ||
15 | + return this.userBJID; | ||
16 | +}; | ||
17 | + | ||
18 | +ProfileSchema.methods.serialize = function () { | ||
19 | + const data = this.toJSON(); | ||
20 | + return data; | ||
21 | +}; | ||
22 | +const Profile = mongoose.model("Profile", ProfileSchema); | ||
23 | +module.exports = Profile; |
... | @@ -3,7 +3,6 @@ const cheerio = require("cheerio"); | ... | @@ -3,7 +3,6 @@ const cheerio = require("cheerio"); |
3 | const StringToDate = require("./StringToDate"); | 3 | const StringToDate = require("./StringToDate"); |
4 | /* | 4 | /* |
5 | ToDO | 5 | ToDO |
6 | -- 유저 네임 검증 | ||
7 | - 예외 처리 | 6 | - 예외 처리 |
8 | */ | 7 | */ |
9 | exports.getBJ = async function (userid) { | 8 | exports.getBJ = async function (userid) { | ... | ... |
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"); |
-
Please register or login to post a comment