송용우

Merge commit '95c511dc' into develop

...@@ -4,6 +4,7 @@ import './App.css'; ...@@ -4,6 +4,7 @@ import './App.css';
4 import LoginPage from './pages/LoginPage'; 4 import LoginPage from './pages/LoginPage';
5 import RegisterPage from './pages/RegisterPage'; 5 import RegisterPage from './pages/RegisterPage';
6 import HomePage from './pages/HomePage'; 6 import HomePage from './pages/HomePage';
7 +import SettingPage from './pages/SettingPage';
7 8
8 function App() { 9 function App() {
9 return ( 10 return (
...@@ -11,6 +12,7 @@ function App() { ...@@ -11,6 +12,7 @@ function App() {
11 <Route component={HomePage} path={['/@:username', '/']} exact /> 12 <Route component={HomePage} path={['/@:username', '/']} exact />
12 <Route component={LoginPage} path="/login" /> 13 <Route component={LoginPage} path="/login" />
13 <Route component={RegisterPage} path="/register" /> 14 <Route component={RegisterPage} path="/register" />
15 + <Route component={SettingPage} path="/setting" />
14 </> 16 </>
15 ); 17 );
16 } 18 }
......
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;
...@@ -3,6 +3,7 @@ import styled from 'styled-components'; ...@@ -3,6 +3,7 @@ import styled from 'styled-components';
3 import Responsive from './Responsive'; 3 import Responsive from './Responsive';
4 import Button from './Button'; 4 import Button from './Button';
5 import { Link } from 'react-router-dom'; 5 import { Link } from 'react-router-dom';
6 +import Categories from './Categories';
6 7
7 const HeaderBlock = styled.div` 8 const HeaderBlock = styled.div`
8 position: fixed; 9 position: fixed;
...@@ -35,7 +36,7 @@ const UserInfo = styled.div` ...@@ -35,7 +36,7 @@ const UserInfo = styled.div`
35 margin-right: 1rem; 36 margin-right: 1rem;
36 `; 37 `;
37 38
38 -const Header = ({ user, onLogout }) => { 39 +const Header = ({ user, onLogout, category, onSelect }) => {
39 return ( 40 return (
40 <> 41 <>
41 <HeaderBlock> 42 <HeaderBlock>
...@@ -43,6 +44,11 @@ const Header = ({ user, onLogout }) => { ...@@ -43,6 +44,11 @@ const Header = ({ user, onLogout }) => {
43 <Link to="/" className="logo"> 44 <Link to="/" className="logo">
44 작심삼일 45 작심삼일
45 </Link> 46 </Link>
47 + <Categories
48 + category={category}
49 + onSelect={onSelect}
50 + className="right"
51 + />
46 {user ? ( 52 {user ? (
47 <div className="right"> 53 <div className="right">
48 <UserInfo>{user.username}</UserInfo> 54 <UserInfo>{user.username}</UserInfo>
......
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 +const BJIDFormBlock = styled.div`
6 + width: 100%;
7 + border-top: 1px solid ${palette.gray[2]};
8 + padding-top: 2rem;
9 + h4 {
10 + color: ${palette.gray[8]};
11 + margin-top: 0;
12 + margin-bottom: 0.5rem;
13 + }
14 +`;
15 +
16 +const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => {
17 + return (
18 + <BJIDFormBlock>
19 + <h4>백준 아이디</h4>
20 + <form onSubmit={onBJIDSubmit}>
21 + <input
22 + name="userBJID"
23 + onChange={onChange}
24 + value={profile.userBJID}
25 + placeholder="백준 아이디"
26 + />
27 + <button type="submit">등록</button>
28 + </form>
29 + <button onClick={onSyncBJIDSubmit}>동기화</button>
30 + </BJIDFormBlock>
31 + );
32 +};
33 +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 +
7 +const SettingFormBlock = styled.div`
8 + h3 {
9 + margin: 0;
10 + color: ${palette.gray[8]};
11 + margin-bottom: 1rem;
12 + }
13 + background: ${palette.gray[2]};
14 + margin: 0 auto;
15 + display: flex;
16 + flex-direction: column;
17 +`;
18 +const StyledInput = styled.input`
19 + font-size: 1rem;
20 + border: none;
21 + border-bottom: 1px solid ${palette.gray[5]};
22 + padding-bottom: 0.5rem;
23 + outline: none;
24 + &:focus {
25 + color: $oc-teal-7;
26 + border-bottom: 1px solid ${palette.gray[7]};
27 + }
28 + & + & {
29 + margin-top: 1rem;
30 + }
31 +`;
32 +const SectionContainer = styled.div`
33 + display: flex;
34 +`;
35 +
36 +const SettingForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => {
37 + return (
38 + <SettingFormBlock>
39 + <SectionContainer>
40 + <h3>{profile.username}</h3>
41 + <p>입력</p>
42 + </SectionContainer>
43 +
44 + <SectionContainer>
45 + <BJIDForm
46 + profile={profile}
47 + onChange={onChange}
48 + onBJIDSubmit={onBJIDSubmit}
49 + onSyncBJIDSubmit={onSyncBJIDSubmit}
50 + />
51 + </SectionContainer>
52 +
53 + <SectionContainer>
54 + <h3>친구</h3>
55 + <StyledInput name="BJID" placeholder="친구 아이디" />
56 + <Button>추가</Button>
57 + </SectionContainer>
58 + <h3>친구 리스트</h3>
59 + </SettingFormBlock>
60 + );
61 +};
62 +
63 +export default SettingForm;
...@@ -4,6 +4,7 @@ import Header from '../../components/common/Header'; ...@@ -4,6 +4,7 @@ import Header from '../../components/common/Header';
4 import { logout } from '../../modules/user'; 4 import { logout } from '../../modules/user';
5 const HeaderContainer = () => { 5 const HeaderContainer = () => {
6 const { user } = useSelector(({ user }) => ({ user: user.user })); 6 const { user } = useSelector(({ user }) => ({ user: user.user }));
7 +
7 const dispatch = useDispatch(); 8 const dispatch = useDispatch();
8 const onLogout = () => { 9 const onLogout = () => {
9 dispatch(logout()); 10 dispatch(logout());
......
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 +} from '../../modules/profile';
10 +import SettingForm from '../../components/setting/SettingForm';
11 +import { sync } from '../../../node_modules/fast-glob/index';
12 +const SettingContainer = ({ history }) => {
13 + const dispatch = useDispatch();
14 + const [error, setError] = useState(null);
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 + console.log('1');
46 + let username = JSON.parse(user).username;
47 + dispatch(getPROFILE({ username }));
48 + //Do Init Form
49 + }, [dispatch]);
50 +
51 + return (
52 + <SettingForm
53 + type="setting"
54 + onChange={onChange}
55 + onBJIDSubmit={onBJIDSubmit}
56 + onSyncBJIDSubmit={onSyncBJIDSubmit}
57 + profile={profile}
58 + ></SettingForm>
59 + );
60 +};
61 +
62 +export default withRouter(SettingContainer);
1 -import axios from './axios'; 1 +import axios from 'axios';
2 const client = axios.create(); 2 const client = axios.create();
3 export default client; 3 export default client;
......
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 });
...@@ -5,19 +5,19 @@ import createRequestSaga, { ...@@ -5,19 +5,19 @@ import createRequestSaga, {
5 createRequestActionTypes, 5 createRequestActionTypes,
6 } from '../lib/createRequestSaga'; 6 } from '../lib/createRequestSaga';
7 import * as authAPI from '../lib/api/auth'; 7 import * as authAPI from '../lib/api/auth';
8 -const CHAGE_FIELD = 'auth/CHANGE_FIELD'; 8 +const CHANGE_FIELD = 'auth/CHANGE_FIELD';
9 const INITIALIZE_FORM = 'auth/INITIALIZE_FORM'; 9 const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';
10 10
11 -const REGISTER = 'auth/REGISTER'; 11 +const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] = createRequestActionTypes(
12 -const REGISTER_SUCCESS = 'auth/REGISTER_SUCCESS'; 12 + 'auth/REGISTER',
13 -const REGISTER_FAILURE = 'auth/REGISTER_FAILURE'; 13 +);
14 14
15 -const LOGIN = 'auth/LOGIN'; 15 +const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] = createRequestActionTypes(
16 -const LOGIN_SUCCESS = 'auth/LOGIN_SUCCESS'; 16 + 'auth/REGISTER',
17 -const LOGIN_FAILURE = 'auth/LOGIN_FAILURE'; 17 +);
18 18
19 export const changeField = createAction( 19 export const changeField = createAction(
20 - CHAGE_FIELD, 20 + CHANGE_FIELD,
21 ({ form, key, value }) => ({ 21 ({ form, key, value }) => ({
22 form, 22 form,
23 key, 23 key,
...@@ -59,7 +59,7 @@ export function* authSaga() { ...@@ -59,7 +59,7 @@ export function* authSaga() {
59 59
60 const auth = handleActions( 60 const auth = handleActions(
61 { 61 {
62 - [CHAGE_FIELD]: (state, { payload: { form, key, value } }) => 62 + [CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
63 produce(state, (draft) => { 63 produce(state, (draft) => {
64 draft[form][key] = value; 64 draft[form][key] = value;
65 }), 65 }),
......
...@@ -3,15 +3,17 @@ import { all } from 'redux-saga/effects'; ...@@ -3,15 +3,17 @@ import { all } from 'redux-saga/effects';
3 import auth, { authSaga } from './auth'; 3 import auth, { authSaga } from './auth';
4 import loading from './loading'; 4 import loading from './loading';
5 import user, { userSaga } from './user'; 5 import user, { userSaga } from './user';
6 +import profile, { profileSaga } from './profile';
6 7
7 const rootReducer = combineReducers({ 8 const rootReducer = combineReducers({
8 auth, 9 auth,
9 loading, 10 loading,
10 user, 11 user,
12 + profile,
11 }); 13 });
12 14
13 export function* rootSaga() { 15 export function* rootSaga() {
14 - yield all([authSaga(), userSaga()]); 16 + yield all([authSaga(), userSaga(), profileSaga()]);
15 } 17 }
16 18
17 export default rootReducer; 19 export default rootReducer;
......
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 +
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 +);
...@@ -6,7 +6,7 @@ const HomePage = () => { ...@@ -6,7 +6,7 @@ const HomePage = () => {
6 return ( 6 return (
7 <div> 7 <div>
8 <HeaderContainer /> 8 <HeaderContainer />
9 - <Button>test</Button> 9 + <Button>home</Button>
10 </div> 10 </div>
11 ); 11 );
12 }; 12 };
......
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;
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
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 |
...@@ -29,8 +29,10 @@ ...@@ -29,8 +29,10 @@
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 | 유저 정보 받아오기 | POST | api/profile/getprofile | 바로가기 | JWT |
32 | profile | 추천 문제 조회 | GET | api/profile/recommend:id | 바로가기 | None | 33 | profile | 추천 문제 조회 | GET | api/profile/recommend:id | 바로가기 | None |
33 -| notify | 슬랙 메시지 전송 요청 | POST | api/notify/slack | 바로가기 | Jwt Token | 34 +| notify | 슬랙 메시지 전송 요청 | POST | api/notify/ |
35 +| slack | 바로가기 | Jwt Token |
34 | auth | 로그인 | POST | api/auth/login | 바로가기 | None | 36 | auth | 로그인 | POST | api/auth/login | 바로가기 | None |
35 | auth | 로그아웃 | POST | api/auth/logout | 바로가기 | JWT Token | 37 | auth | 로그아웃 | POST | api/auth/logout | 바로가기 | JWT Token |
36 | auth | 회원가입 | POST | api/auth/register | 바로가기 | None | 38 | auth | 회원가입 | POST | api/auth/register | 바로가기 | None |
......
...@@ -88,7 +88,6 @@ exports.login = async (ctx) => { ...@@ -88,7 +88,6 @@ exports.login = async (ctx) => {
88 GET api/auth/check 88 GET api/auth/check
89 */ 89 */
90 exports.check = async (ctx) => { 90 exports.check = async (ctx) => {
91 - console.log(ctx.state);
92 const { user } = ctx.state; 91 const { user } = ctx.state;
93 if (!user) { 92 if (!user) {
94 ctx.status = 401; 93 ctx.status = 401;
......
...@@ -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 {
......
...@@ -3,14 +3,11 @@ const User = require("../models/user"); ...@@ -3,14 +3,11 @@ const User = require("../models/user");
3 3
4 const jwtMiddleware = async (ctx, next) => { 4 const jwtMiddleware = async (ctx, next) => {
5 const token = ctx.cookies.get("access_token"); 5 const token = ctx.cookies.get("access_token");
6 - console.log("1"); 6 +
7 - console.log(token);
8 if (!token) { 7 if (!token) {
9 - console.log("1");
10 return next(); 8 return next();
11 } 9 }
12 try { 10 try {
13 - console.log("1");
14 const decoded = jwt.verify(token, process.env.JWT_SECRET); 11 const decoded = jwt.verify(token, process.env.JWT_SECRET);
15 ctx.state.user = { 12 ctx.state.user = {
16 _id: decoded._id, 13 _id: decoded._id,
...@@ -25,7 +22,7 @@ const jwtMiddleware = async (ctx, next) => { ...@@ -25,7 +22,7 @@ const jwtMiddleware = async (ctx, next) => {
25 httpOnly: true, 22 httpOnly: true,
26 }); 23 });
27 } 24 }
28 - console.log(decoded); 25 +
29 return next(); 26 return next();
30 } catch (e) { 27 } catch (e) {
31 return next(); 28 return next();
......