송용우

Merge commit 'ecda5049' into feature/frontend_page

Showing 41 changed files with 835 additions and 357 deletions
MIT License
Copyright (c) 2020 Yong-Woo Song
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
# Jaksimsamil
![issue badge](https://img.shields.io/github/issues/FacerAin/OSS-Jaksimsamil)
![fork badge](https://img.shields.io/github/forks/FacerAin/OSS-Jaksimsamil)
![star badge](https://img.shields.io/github/stars/FacerAin/OSS-Jaksimsamil)
![license badge](https://img.shields.io/github/license/FacerAin/OSS-Jaksimsamil)
## Project Overview
> **Jaksaimsamil Algorithm Study Helper Service**
>
> 작심삼일 알고리즘 문제풀이 도우미 서비스<br/>
>
> > 알고리즘 문제 풀이 스터디를 꾸준히 할 수 있게 돕는 웹 서비스입니다.
> > <br> [링크](http://facerain.dcom.club)에서 직접 사용해 보세요!
![그림1](https://user-images.githubusercontent.com/16442978/85690047-236d1d00-b70e-11ea-8d2b-480593c0daf3.png)
![그림2](https://user-images.githubusercontent.com/16442978/85690058-2536e080-b70e-11ea-98cd-45fdf04084ce.png)
## Features (ver.1.0.0)
- 회원가입/로그인 제공
- Online Judge 연동 가능 (Baekjoon)
- 나의 학습 현황 한눈에 보기
- 추천 문제 제공
- Slack 알리미
## Upcoming Features
- 친구 추가
- 친구와의 경쟁
- 그룹 추가
- 그룹 추천
- 개선된 문제 추천 (사용자 실력 맞춤형)
## Usages
#### 회원
1. 로그인하여 서비스에 접속 할 수 있습니다.
2. 서비스가 처음이라면, 회원가입을 하세요.
<br>
#### 설정
1. 백준 아이디를 등록하고 동기화하세요. [상세]()
2. 슬랙 HOOK URL을 등록하세요. [상세]()
3. 일일 목표량을 등록하세요.
## Getting Started
1. Clone
```
git clone https://github.com/FacerAin/OSS-Jaksimsamil.git
```
2. Install MongoDB(Ubuntu)
```
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo service mongod start
```
3. Set Serverfile
```
cd Jaksimsamil-server
touch .env
---TYPE THIS IN FILE----
SERVER_PORT= ###
MONGO_URL= ###
JWT_SECRET= ###
```
4. Start Node Server
```
cd Jaksimsamil-server
sudo npm install
npm start
```
[링크](/jaksimsamil-server/README.md)에서 API 제공 목록을 볼 수 있습니다.
<br>
5. Set Front-end page
```
cd Jaksimsamil-server
sudo npm install
npm start #Start React
```
## Contributing
컨트리뷰션은 언제나 환영입니다. 다음 절차를 지켜주세요!
1. Fork the Project
2. Create your Feature Branch
3. Commit our changes
4. Push to Branch
5. Open a Pull Request
## License
- MIT LICENCE
......
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
This diff could not be displayed because it is too large.
......@@ -16,7 +16,7 @@
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-scripts": "^3.4.3",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-devtools-extension": "^2.13.8",
......
......@@ -3,42 +3,111 @@ import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import palette from '../../lib/styles/palette';
import AuthForm from '../auth/AuthForm';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
background: palette.gray[2],
padding: theme.spacing(8),
},
paper: {
padding: theme.spacing(2),
padding: theme.spacing(8),
margin: 'auto',
textAlign: 'center',
color: theme.palette.text.secondary,
},
}));
const HomeForm = () => {
const HomeForm = ({ PSdata, goalNum }) => {
const classes = useStyles();
return (
return PSdata ? (
<div className={classes.root}>
<Grid container spacing={3}>
<Grid container spacing={5}>
<Grid item xs={12}>
<Paper className={classes.paper}>xs=12</Paper>
<Paper className={classes.paper}>
<h1>{PSdata.recommend_data.problem_number}</h1>
<h1>{PSdata.recommend_data.problem_title}</h1>
<a
href={'http://www.boj.kr/' + PSdata.recommend_data.problem_number}
>
바로가기
</a>
<h3>오늘의 추천 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1>{PSdata.presentNum + '/' + goalNum}</h1>
<h3>오늘 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>xs=6</Paper>
<Paper className={classes.paper}>
<h1>{PSdata.latestSolve.problem_number}</h1>
<h1>{PSdata.latestSolve.problem_title}</h1>
<h3>마지막으로 문제</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.weekNum}</h1>
<h3>7</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.monthNum}</h1>
<h3>30</h3>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1>{PSdata.totalNum}</h1>
<h3>전체</h3>
</Paper>
</Grid>
</Grid>
</div>
) : (
<div className={classes.root}>
<Grid container spacing={5}>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h1></h1>
<h3>오늘의 추천 문제</h3>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.paper}>xs=6</Paper>
<Paper className={classes.paper}>
<h1></h1>
<h3>오늘</h3>
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.paper}>xs=3</Paper>
<Grid item xs={6}>
<Paper className={classes.paper}>
<h1></h1>
<h3>마지막 </h3>
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.paper}>xs=3</Paper>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>7</h3>
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.paper}>xs=3</Paper>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>30</h3>
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.paper}>xs=3</Paper>
<Grid item xs={4}>
<Paper className={classes.paper}>
<h1></h1>
<h3>전체</h3>
</Paper>
</Grid>
</Grid>
</div>
......
......@@ -9,13 +9,16 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => {
const classes = useStyles();
return (
<div>
<form onSubmit={onBJIDSubmit}>
<form>
<TextField
name="userBJID"
onChange={onChange}
......@@ -23,11 +26,21 @@ const BJIDForm = ({ onChange, onBJIDSubmit, profile, onSyncBJIDSubmit }) => {
placeholder="백준 아이디"
label="백준 아이디"
/>
<Button variant="outlined" type="submit">
등록
</Button>
</form>
<Button variant="outlined" onClick={onSyncBJIDSubmit}>
<Button
className={classes.button}
variant="outlined"
onClick={onBJIDSubmit}
color="primary"
>
등록
</Button>
<Button
className={classes.button}
variant="outlined"
onClick={onSyncBJIDSubmit}
color="secondary"
>
동기화
</Button>
</div>
......
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const GoalNumForm = ({ onChange, profile, onGoalNumSubmit }) => {
const classes = useStyles();
return (
<div>
<form>
<TextField
name="goalNum"
type="number"
onChange={onChange}
value={profile.goalNum}
placeholder="일일 목표"
label="일일 목표"
InputLabelProps={{
shrink: true,
}}
/>
</form>
<Button
className={classes.button}
onClick={onGoalNumSubmit}
color="primary"
variant="outlined"
>
등록
</Button>
</div>
);
};
export default GoalNumForm;
......@@ -2,40 +2,54 @@ import React from 'react';
import palette from '../../lib/styles/palette';
import BJIDForm from './BJIDForm';
import SlackForm from './SlackForm';
import GoalNumForm from './GoalNumForm';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import CircularProgress from '@material-ui/core/CircularProgress';
import styled from 'styled-components';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
background: palette.gray[2],
padding: theme.spacing(8),
},
paper: {
padding: theme.spacing(8),
margin: 'auto',
textAlign: 'center',
padding: 30,
},
}));
const LoadingParentStyle = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 20px;
`;
const SettingForm = ({
onChange,
onBJIDSubmit,
onSlackURLSubmit,
profile,
onSyncBJIDSubmit,
onGoalNumSubmit,
isLoading,
}) => {
const classes = useStyles();
return (
return isLoading ? (
<LoadingParentStyle>
<CircularProgress className={classes.loading} />
</LoadingParentStyle>
) : (
<div className={classes.root}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h3>{profile.username}</h3>
</Paper>
</Grid>
<Grid container item xs={12}>
<Grid container spacing={5}>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>백준 아이디</h1>
<BJIDForm
profile={profile}
onChange={onChange}
......@@ -45,8 +59,9 @@ const SettingForm = ({
</Paper>
</Grid>
<Grid container item xs={12}>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>슬랙 Hook URL</h1>
<SlackForm
profile={profile}
onChange={onChange}
......@@ -54,6 +69,17 @@ const SettingForm = ({
/>
</Paper>
</Grid>
<Grid container item xs={6}>
<Paper className={classes.paper} elevation={3}>
<h1>일일 목표</h1>
<GoalNumForm
profile={profile}
onChange={onChange}
onGoalNumSubmit={onGoalNumSubmit}
/>
</Paper>
</Grid>
</Grid>
</div>
);
......
......@@ -10,13 +10,16 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(1),
},
},
button: {
margin: theme.spacing(1),
},
}));
const SlackForm = ({ onChange, profile, onSlackURLSubmit }) => {
const classes = useStyles();
return (
<div>
<form onSubmit={onSlackURLSubmit}>
<form>
<TextField
name="slackWebHookURL"
onChange={onChange}
......@@ -24,10 +27,16 @@ const SlackForm = ({ onChange, profile, onSlackURLSubmit }) => {
placeholder="슬랙 Webhook URL"
label="슬랙 Webhook URL"
/>
<Button variant="outlined" type="submit">
등록
</Button>
</form>
<Button
className={classes.button}
onClick={onSlackURLSubmit}
variant="outlined"
type="submit"
color="primary"
>
등록
</Button>
</div>
);
};
......
......@@ -2,21 +2,35 @@ import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import HomeForm from '../../components/home/HomeForm';
import { getPROFILE } from '../../modules/profile';
import { analyzeBJ } from '../../lib/util/analyzeBJ';
import { getPROFILE, initializeProfile } from '../../modules/profile';
const HomeContainer = ({ history }) => {
const dispatch = useDispatch();
const { user, profile } = useSelector(({ user, profile }) => ({
user: user.user,
profile: profile,
}));
useEffect(() => {}, [profile.solvedBJ]);
useEffect(() => {
if (!user) {
alert('로그인이 필요합니다 ');
history.push('/login');
} else {
let username = user.username;
dispatch(getPROFILE({ username }));
return () => {
dispatch(initializeProfile());
};
}
}, [dispatch, user, history]);
useEffect(() => {
console.log(profile);
}, [profile]);
useEffect(() => {
if (user) {
let username = user.username;
dispatch(getPROFILE({ username }));
}
}, [dispatch, user]);
return <HomeForm />;
return <HomeForm PSdata={profile.solvedBJ_date} goalNum={profile.goalNum} />;
};
export default withRouter(HomeContainer);
......
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {
changeField,
......@@ -8,15 +9,21 @@ import {
syncBJID,
initializeProfile,
setSLACK,
setGOALNUM,
} from '../../modules/profile';
import SettingForm from '../../components/setting/SettingForm';
const SettingContainer = ({ history }) => {
const [isLoading, setLoading] = useState(false);
const dispatch = useDispatch();
const { user, profile } = useSelector(({ user, profile }) => ({
user: user.user,
profile: profile,
}));
const { user, profile, loading } = useSelector(
({ user, profile, loading }) => ({
user: user.user,
profile: profile,
loading: loading,
}),
);
const onChange = (e) => {
const { value, name } = e.target;
......@@ -33,6 +40,13 @@ const SettingContainer = ({ history }) => {
let username = profile.username;
dispatch(syncBJID({ username }));
};
const onGoalNumSubmit = (e) => {
e.preventDefault();
let username = profile.username;
let goalNum = profile.goalNum;
dispatch(setGOALNUM({ username, goalNum }));
};
const onSlackURLSubmit = (e) => {
e.preventDefault();
let username = profile.username;
......@@ -51,7 +65,7 @@ const SettingContainer = ({ history }) => {
useEffect(() => {
if (!user) {
alert('로그인이 필요합니다 ');
history.push('/');
history.push('/login');
} else {
let username = user.username;
dispatch(getPROFILE({ username }));
......@@ -60,16 +74,27 @@ const SettingContainer = ({ history }) => {
};
}
}, [dispatch, user, history]);
useEffect(() => {
if (loading['profile/SYNC_BJID'] == true) {
setLoading(true);
} else {
setLoading(false);
}
}, [dispatch, loading]);
return (
<SettingForm
type="setting"
onChange={onChange}
onBJIDSubmit={onBJIDSubmit}
onSyncBJIDSubmit={onSyncBJIDSubmit}
onSlackURLSubmit={onSlackURLSubmit}
profile={profile}
></SettingForm>
<div>
<SettingForm
type="setting"
onChange={onChange}
onBJIDSubmit={onBJIDSubmit}
onSyncBJIDSubmit={onSyncBJIDSubmit}
onSlackURLSubmit={onSlackURLSubmit}
onGoalNumSubmit={onGoalNumSubmit}
profile={profile}
isLoading={isLoading}
></SettingForm>
</div>
);
};
......
......@@ -17,6 +17,11 @@ const [
SET_SLACK_FAILURE,
] = createRequestActionTypes('/profile/SET_SLACK');
const [
SET_GOALNUM,
SET_GOALNUM_SUCCESS,
SET_GOALNUM_FAILURE,
] = createRequestActionTypes('/profile/SET_GOALNUM');
const [
GET_PROFILE,
GET_PROFILE_SUCCESS,
GET_PROFILE_FAILURE,
......@@ -31,6 +36,7 @@ export const initializeProfile = createAction(INITIALIZE);
export const syncBJID = createAction(SYNC_BJID, ({ username }) => ({
username,
}));
export const setSLACK = createAction(
SET_SLACK,
({ username, slackWebHookURL }) => ({
......@@ -38,6 +44,14 @@ export const setSLACK = createAction(
slackWebHookURL,
}),
);
export const setGOALNUM = createAction(
SET_GOALNUM,
({ username, goalNum }) => ({
username,
goalNum,
}),
);
export const setBJID = createAction(SET_BJID, ({ username, userBJID }) => ({
username,
userBJID,
......@@ -58,16 +72,21 @@ const initialState = {
friendList: [],
profileError: '',
slackWebHookURL: '',
solvedBJ_date: '',
goalNum: '',
};
const getPROFILESaga = createRequestSaga(GET_PROFILE, profileAPI.getPROFILE);
const setBJIDSaga = createRequestSaga(SET_BJID, profileAPI.setBJID);
const setSLACKSaga = createRequestSaga(SET_SLACK, profileAPI.setPROFILE);
const setGOALNUMSaga = createRequestSaga(SET_GOALNUM, profileAPI.setPROFILE);
const syncBJIDSaga = createRequestSaga(SYNC_BJID, profileAPI.syncBJ);
export function* profileSaga() {
yield takeLatest(SET_BJID, setBJIDSaga);
yield takeLatest(GET_PROFILE, getPROFILESaga);
yield takeLatest(SYNC_BJID, syncBJIDSaga);
yield takeLatest(SET_SLACK, setSLACKSaga);
yield takeLatest(SET_GOALNUM, setGOALNUMSaga);
}
export default handleActions(
......@@ -80,7 +99,15 @@ export default handleActions(
[GET_PROFILE_SUCCESS]: (
state,
{
payload: { username, userBJID, solvedBJ, friendList, slackWebHookURL },
payload: {
username,
userBJID,
solvedBJ,
friendList,
slackWebHookURL,
solvedBJ_date,
goalNum,
},
},
) => ({
...state,
......@@ -90,6 +117,8 @@ export default handleActions(
friendList: friendList,
profileError: null,
slackWebHookURL: slackWebHookURL,
solvedBJ_date: solvedBJ_date,
goalNum: goalNum,
}),
[GET_PROFILE_FAILURE]: (state, { payload: error }) => ({
...state,
......@@ -114,6 +143,14 @@ export default handleActions(
...state,
profileError: error,
}),
[SET_GOALNUM_SUCCESS]: (state, { payload: { goalNum } }) => ({
...state,
goalNum: goalNum,
}),
[SET_GOALNUM_FAILURE]: (state, { payload: error }) => ({
...state,
profileError: error,
}),
[SYNC_BJID_SUCCESS]: (state, { payload: { solvedBJ } }) => ({
...state,
solvedBJ,
......
# Jaksimsamil API Documentation
## Overview
- TBA
## URL
- TBA
## Usage
- TBA
## Example
- TBA
## API Table
| group | description | method | URL | Detail | Auth |
| ------- | --------------------------- | --------- | ------------------------ | -------- | --------- |
| user | 유저 등록 | POST | api/user | 바로가기 | JWT Token |
| user | 유저 삭제 | DELETE | api/user:id | 바로가기 | JWT Token |
| user | 특정 유저 조회 | GET | api/user:id | 바로가기 | None |
| user | 전체 유저 조회 | GET | api/user | 바로가기 | JWT Token |
| friend | 유저 친구 등록 | POST | api/friend | 바로가기 | JWT Token |
| friend | 유저의 친구 조회 | GET | api/friend:id | 바로가기 | None |
| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | 바로가기 | None |
| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | 바로가기 | None |
| profile | 유저 정보 수정 | POST | api/profile/setprofile | 바로가기 | JWT TOKEN |
| profile | 유저 정보 받아오기 | POST | api/profile/getprofile | 바로가기 | JWT |
| profile | 추천 문제 조회 | GET | api/profile/recommend:id | 바로가기 | None |
| notify | 슬랙 메시지 전송 요청 | POST | api/notify/ |
| slack | 바로가기 | Jwt Token |
| auth | 로그인 | POST | api/auth/login | 바로가기 | None |
| auth | 로그아웃 | POST | api/auth/logout | 바로가기 | JWT Token |
| auth | 회원가입 | POST | api/auth/register | 바로가기 | None |
| auth | 로그인 확인 | GET | api/auth/check | 바로가기 | None |
# Jaksimsamil Server Documentation
## Overview
- KOA 프레임워크 기반의 REST-API로 동작합니다.
- API 문서는 아래를 참고해주세요.
## Usage
- Starting Server
```
npm install
npm update
node index.js
```
## Example
```
POST http://facerain.dcom.club/profile/getprofile
{
username: 'syw5141',
}
```
## API Table
| group | description | method | URL | Detail | Auth |
| ------- | -------------------------------------- | ------ | ----------------------- | -------------------------------------- | --------- |
| profile | 유저가 푼 문제 조회(백준) | GET | api/profile/solvedBJ:id | [바로가기](/src/api/profile/README.md) | None |
| profile | 유저가 푼 문제 동기화(백준) | PATCH | api/profile/syncBJ | [바로가기](/src/api/profile/README.md) | None |
| profile | 유저 정보 수정 | POST | api/profile/setprofile | [바로가기](/src/api/profile/README.md) | JWT TOKEN |
| profile | 유저 정보 받아오기 | POST | api/profile/getprofile | [바로가기](/src/api/profile/README.md) | JWT |
| profile | 추천 문제 조회 | POST | api/profile/recommend | [바로가기](/src/api/profile/README.md) | None |
| profile | 친구 추가 | POST | /api/profile/addfriend | [바로가기](/src/api/profile/README.md) | JWT TOKEN |
| notify | 슬랙 메시지 전송 요청 (목표 성취 여부) | POST | api/notify/goal | [바로가기](/src/api/notify/README.md) | Jwt Token |
| notify | 슬랙 메시지 전송 요청 (문제 추천) | POST | api/notify/recommend | [바로가기](/src/api/notify/README.md) | None |
| auth | 로그인 | POST | api/auth/login | [바로가기](/src/api/auth/README.md) | None |
| auth | 로그아웃 | POST | api/auth/logout | [바로가기](/src/api/auth/README.md) | JWT Token |
| auth | 회원가입 | POST | api/auth/register | [바로가기](/src/api/auth/README.md) | None |
| auth | 로그인 확인 | GET | api/auth/check | [바로가기](/src/api/auth/README.md) | None |
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -5,7 +5,7 @@
"license": "MIT",
"dependencies": {
"axios": "^0.19.2",
"bcrypt": "^4.0.1",
"bcrypt": "^3.0.0",
"body-parser": "^1.19.0",
"cheerio": "^1.0.0-rc.3",
"cookie-parser": "^1.4.5",
......@@ -15,11 +15,11 @@
"iconv": "^3.0.0",
"joi": "^14.3.1",
"jsonwebtoken": "^8.5.1",
"koa": "^2.12.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"koa-morgan": "^1.0.1",
"koa-router": "^9.0.1",
"mongoose": "^5.9.17",
"mongoose": "^5.9.20",
"morgan": "^1.10.0",
"node-schedule": "^1.3.2",
"path": "^0.12.7",
......@@ -29,11 +29,11 @@
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.1.0",
"eslint": "^7.3.1",
"nodemon": "^2.0.4"
},
"scripts": {
"start": "node src",
"start": "node ./index.js",
"start:dev": "nodemon --watch src/ src/index.js"
}
}
......
......@@ -3,7 +3,7 @@ const api = new Router();
const auth = require("./auth");
const friend = require("./friend");
const notify = require("./profile");
const notify = require("./notify");
const user = require("./user");
const profile = require("./profile");
......
const Router = require("koa-router");
const notify = new Router();
notify.post("/slack");
const slackCtrl = require("./slack.ctrl");
notify.post("/slack/goal", slackCtrl.slackGoal);
notify.post("/slack/recommend", slackCtrl.slackRecommend);
module.exports = notify;
......
const Profile = require("../../models/profile");
const sendSlack = require("../../util/sendSlack");
const problem_set = require("../../data/problem_set");
const compareBJ = require("../../util/compareBJ");
/*
POST api/notify/slack/goal
{
username: "username"
}
*/
exports.slackGoal = async (ctx) => {
try {
const { username } = ctx.request.body;
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let slackURL = profile.getslackURL();
if (!slackURL) {
ctx.status = 401;
return;
}
let goalNum = profile.getgoalNum();
let todayNum = profile.getTodaySovled();
let message = "";
if (goalNum < todayNum) {
message =
"오늘의 목표 " +
goalNum +
"문제 중 " +
todayNum +
"문제를 풀었습니다." +
"\n" +
"잘하셨습니다!";
} else {
message =
"오늘의 목표 " +
goalNum +
"문제 중 " +
todayNum +
"문제를 풀었습니다." +
"\n" +
"분발하세요!";
}
sendSlack.send(message, slackURL);
} catch (e) {
ctx.throw(500, e);
}
};
/*
POST api/notify/slack/recommend
{
username: "username"
}
*/
exports.slackRecommend = async (ctx) => {
try {
console.log("1");
const { username } = ctx.request.body;
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let slackURL = profile.getslackURL();
if (!slackURL) {
ctx.status = 401;
return;
}
let unsolved_data = compareBJ.compareBJ(
profile.getBJdata(),
problem_set.problem_set
);
let recommendData = compareBJ.randomItem(unsolved_data);
if (!recommendData) {
ctx.status = 401;
return;
}
let message =
"오늘의 추천 문제는 " +
recommendData.problem_number +
"번 " +
" <https://www.boj.kr/" +
recommendData.problem_number +
"|" +
recommendData.problem_title +
">" +
" 입니다.";
sendSlack.send(message, slackURL);
} catch (e) {
ctx.throw(500, e);
}
};
const Router = require("koa-router");
const profile = new Router();
const profileCtrl = require("./profile.ctrl");
profile.post("/solved:id");
profile.get("/solvednum:id");
profile.get("/recommendps:id");
profile.post("/recommend", profileCtrl.recommend);
profile.patch("/syncBJ", profileCtrl.syncBJ);
profile.post("/setprofile", profileCtrl.setProfile);
profile.post("/getprofile", profileCtrl.getProfile);
......
......@@ -2,6 +2,9 @@ const Profile = require("../../models/profile");
const mongoose = require("mongoose");
const getBJ = require("../../util/getBJ");
const Joi = require("joi");
const analyzeBJ = require("../../util/analyzeBJ");
const compareBJ = require("../../util/compareBJ");
const problem_set = require("../../data/problem_set");
const { ObjectId } = mongoose.Types;
......@@ -47,7 +50,7 @@ exports.setProfile = async (ctx) => {
//freindList: Joi.array().items(Joi.string()),
})
.unknown();
console.log(ctx.request.body);
const result = Joi.validate(ctx.request.body, schema);
if (result.error) {
ctx.status = 400;
......@@ -95,9 +98,10 @@ exports.syncBJ = async function (ctx) {
}
const BJID = await profile.getBJID();
let BJdata = await getBJ.getBJ(BJID);
let BJdata_date = await analyzeBJ.analyzeBJ(BJdata);
const updateprofile = await Profile.findOneAndUpdate(
{ username: username },
{ solvedBJ: BJdata },
{ solvedBJ: BJdata, solvedBJ_date: BJdata_date },
{ new: true }
).exec();
ctx.body = updateprofile;
......@@ -105,3 +109,33 @@ exports.syncBJ = async function (ctx) {
ctx.throw(500, e);
}
};
/*
POST /api/proflie/recommend
{
username: 'userid'
}
*/
exports.recommend = async (ctx) => {
const { username } = ctx.request.body;
if (!username) {
ctx.status = 401;
return;
}
try {
const profile = await Profile.findByUsername(username);
if (!profile) {
ctx.status = 401;
return;
}
let unsolved_data = compareBJ.compareBJ(
profile.getBJdata(),
problem_set.problem_set
);
ctx.body = compareBJ.randomItem(unsolved_data);
//데이터가 비었을 떄 예외처리 필요
} catch (e) {
ctx.throw(500, e);
}
};
......
This diff could not be displayed because it is too large.
const mongoose = require("mongoose");
const { Schema } = mongoose;
const GroupSchema = new Schema({
members: { type: [String] },
});
const ChallengeSchema = new Schema({
challengeName: { type: String, required: true },
startDate: { type: Object, required: true },
endDate: { type: Object, required: true },
durationPerSession: { type: String, required: true }, // '1d' means one day per session, '2w' means 2 weeks per session, '3m' means 3 months per session.
goalPerSession: { type: Number, required: true }, // number of problems for one session
groups: { type: [GroupSchema], required: true }, // groups attending challenge, group of only one member supposed to be single
});
ChallengeSchema.statics.findByChallengeName = function (challengeName) {
return this.findOne({ challengeName: challengeName });
};
ChallengeSchema.methods.addNewGroup = function (group) {
this.groups.push(group);
return this.save();
};
ChallengeSchema.methods.removeGroup = function (group_id) {
const idx = this.groups.findIndex((item) => item._id === group_id);
this.groups.splice(idx, 1);
return this.save();
};
ChallengeSchema.methods.getChallengeName = function () {
return this.challengeName;
};
ChallengeSchema.methods.getStartDate = function () {
return this.startDate;
};
ChallengeSchema.methods.getEndDate = function () {
return this.endDate;
};
ChallengeSchema.methods.getDurationPerSession = function () {
return this.durationPerSession;
};
ChallengeSchema.methods.getGoalPerSession = function () {
return this.goalPerSession;
};
ChallengeSchema.methods.getGroups = function () {
return this.groups;
};
ChallengeSchema.methods.serialize = function () {
return this.toJSON();
};
const Challenge = mongoose.model("Challenge", ChallengeSchema);
module.exports = Challenge;
const mongoose=require('mongoose');
const {Schema}=mongoose;
const ProblemSchema=new Schema({
problemNum: {type: Number, required: true, unique: true},
problemTitle: {type: String, required: true},
solvedacLevel: {type: Number},
sumbitNum: {type: Number, required: true},
correctNum: {type: Number, required: true},
category: {type:[String]}
});
ProblemSchema.statics.findByProblemNum=function(problemNum){
return this.findOne({problemNum:problemNum});
}
ProblemSchema.methods.addCategory=function(category){
this.category.push(category);
return this.save();
}
ProblemSchema.methods.removeCategory=function(category){
const idx=this.category.findIndex(item=>item===category);
this.splice(idx,1);
return this.save();
}
ProblemSchema.methods.getProblemNum=function(){
return this.problemNum;
}
ProblemSchema.methods.getProblemTitle=function(){
return this.problemTitle;
}
ProblemSchema.methods.getSolvedacLevel=function(){
return this.solvedacLevel;
}
ProblemSchema.methods.getSumbitNum=function(){
return this.sumbitNum;
}
ProblemSchema.methods.getCorrectNum=function(){
return this.correctNum;
}
ProblemSchema.methods.getCategory=function(){
return this.category;
}
ProblemSchema.methods.serialize=function(){
return this.toJSON();
}
const Problem=mongoose.model('Problem',ProblemSchema);
module.exports=Problem;
\ No newline at end of file
......@@ -6,8 +6,10 @@ const ProfileSchema = new Schema({
username: { type: String, required: true, unique: true },
userBJID: String,
solvedBJ: Object,
solvedBJ_date: Object,
friendList: [String],
slackWebHookURL: String,
goalNum: Number,
});
ProfileSchema.statics.findByUsername = function (username) {
return this.findOne({ username });
......@@ -15,6 +17,20 @@ ProfileSchema.statics.findByUsername = function (username) {
ProfileSchema.methods.getBJID = function () {
return this.userBJID;
};
ProfileSchema.methods.getBJdata = function () {
return this.solvedBJ;
};
ProfileSchema.methods.getslackURL = function () {
return this.slackWebHookURL;
};
ProfileSchema.methods.getgoalNum = function () {
return this.goalNum;
};
ProfileSchema.methods.getTodaySovled = function () {
if (this.solvedBJ_date) {
return this.solvedBJ_date.presentNum;
}
};
ProfileSchema.methods.serialize = function () {
const data = this.toJSON();
......
/*
2. 현재 날짜와의 차이 =>
3. 오늘 푼 문제 => 앞에서부터 순회하면서 데이트 같은거 찾기
3. 최근 일주일간 푼 문제 수 => 앞에서부터 순회하면서 - 값이
4. 추천 문제 => 정규 셋에서 없는거 찾기
5. 날짜별로 묶기.
데이터베이스에서 처리하자
*/
let moment = require('moment');
let moment = require("moment");
const problem_set = require("../data/problem_set");
const compareBJ = require("./compareBJ");
exports.analyzeBJ = function (solvedBJ) {
try {
if (solvedBJ) {
console.log(solvedBJ[0]);
let presentDate = moment();
let presentDate_str = presentDate.format('YYYYMMDD');
let latestDate = moment(solvedBJ[0].solved_date, 'YYYYMMDD');
let difflatest = presentDate.diff(latestDate, 'days');
let presentDate_str = presentDate.format("YYYYMMDD");
let latestDate = moment(solvedBJ[0].solved_date, "YYYYMMDD");
let difflatest = presentDate.diff(latestDate, "days");
let latestSolve = solvedBJ[0];
let solvedBJbyDATE = {};
for (let i = 0; i < solvedBJ.length; i++) {
......@@ -33,14 +25,46 @@ exports.analyzeBJ = function (solvedBJ) {
presentDate_str in solvedBJbyDATE
? solvedBJbyDATE[presentDate_str].length
: 0;
let weekNUM = 0;
let monthNUM = 0;
let totalNUM = 0;
for (let i = 0; i < solvedBJ.length; i++) {
let diffDate = presentDate.diff(
moment(solvedBJ[i].solved_date, "YYYYMMDD"),
"days"
);
if (diffDate <= 7) {
weekNUM++;
monthNUM++;
totalNUM++;
} else if (diffDate <= 31) {
monthNUM++;
totalNUM++;
} else {
totalNUM++;
}
}
let unsolved_data = compareBJ.compareBJ(
solvedBJ,
problem_set.problem_set
);
let recommend_data = compareBJ.randomItem(unsolved_data);
let returnOBJ = {
latestDate: latestDate.format('YYYYMMDD'),
latestDate: latestDate.format("YYYYMMDD"),
difflatest: difflatest,
latestNum: latestNum,
presentNum: presentNum,
weekNum: weekNUM,
monthNum: monthNUM,
totalNum: totalNUM,
solvedBJbyDATE: solvedBJbyDATE,
latestSolve: latestSolve,
recommend_data: recommend_data,
};
console.log(returnOBJ);
return returnOBJ;
}
} catch (e) {
......
/*
집중을 해보자.
새거와 데이터가 있다.
데이터 기준으로 새거에 자료가 있으면 넘어가기
없으면 새 배열에 추가
키만 모아둔 리스트를 만들자.
그렇게 해서
반복은 새거 길이만큼
데이터에 있으면 추가 X
없으면 추가
그렇게 반환
*/
exports.compareBJ = function (solvedBJ_new, problem_set) {
try {
let new_obj = [];
for (let i = 0; i < solvedBJ.length; i++) {
if (solvedBJ_new[i].problem_number in problem_set) {
new_obj.push(solvedBJ_new[i]);
for (let i = 0; i < problem_set.length; i++) {
let found = false;
for (let j = 0; j < solvedBJ_new.length; j++) {
if (solvedBJ_new[j].problem_number == problem_set[i].problem_number) {
found = true;
break;
}
}
if (!found) {
new_obj.push(problem_set[i]);
}
}
console.log(new_obj);
return new_obj;
} catch (e) {
console.log(e);
}
};
exports.randomItem = function (a) {
return a[Math.floor(Math.random() * a.length)];
};
......
const Slack = require("slack-node"); // 슬랙 모듈 사용
/*
const webhookUri =
"https://hooks.slack.com/services/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"; // Webhook URL
"https://hooks.slack.com/services/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"; // Webhook URL
*/
const slack = new Slack();
slack.setWebhook(webhookUri);
const send = async (message) => {
exports.send = async (message, webhookUri) => {
const slack = new Slack();
slack.setWebhook(webhookUri);
slack.webhook(
{
text: message,
......@@ -16,5 +17,3 @@ const send = async (message) => {
}
);
};
send("hello");
......
var getBJ = require("./getBJ");
var fs = require("fs");
let dataset = [
"1517",
"2448",
"1891",
"1074",
"2263",
"1780",
"11728",
"10816",
"10815",
"2109",
"1202",
"1285",
"2138",
"1080",
"11399",
"1931",
"11047",
"15666",
"15665",
"15664",
"15663",
"15657",
"15656",
"15655",
"15654",
"15652",
"15651",
"15650",
"15649",
"6603",
"10971",
"10819",
"10973",
"10974",
"10972",
"7576",
"1248",
"2529",
"15661",
"14501",
"1759",
"14391",
"14889",
"1182",
"11723",
"1748",
"6064",
"1107",
"3085",
"2309",
"1748",
"14500",
"1107",
"1476",
"3085",
"2309",
"1261",
"13549",
"14226",
"13913",
"1697",
"1967",
"1167",
"11725",
"2250",
"1991",
"7562",
"2178",
"4963",
"2667",
"1707",
"11724",
"1260",
"13023",
"11652",
"1377",
"11004",
"10825",
"2751",
"9461",
"1699",
"9095",
"2225",
"2133",
"11727",
"11726",
"1463",
"2748",
"2747",
"11656",
"10824",
"2743",
"10820",
"10808",
"11655",
"11720",
"1008",
"10951",
"2557",
"1021",
"1966",
"2164",
"10799",
"17413",
"10866",
"1158",
"10845",
"1406",
"1874",
"9012",
"9093",
"10828",
"11721",
"11719",
"11718",
"10953",
"2558",
"10814",
"1181",
"11651",
"11650",
"1427",
"2108",
"10989",
"2751",
"2750",
"1436",
"1018",
"7568",
"2231",
"2798",
"1002",
"3053",
"4153",
"3009",
"1085",
"9020",
"4948",
"1929",
"2581",
"1978",
"2292",
"6064",
"2775",
"10250",
"2869",
"1011",
"1193",
"2839",
"1712",
"1316",
"2941",
"5622",
"2908",
"1152",
"1157",
"2675",
"10809",
"11720",
"11654",
"11729",
"2447",
"3052",
"10818",
"10872",
"10870",
"1065",
"4673",
"15596",
"4344",
"2920",
"8958",
"1546",
"2577",
"2562",
"1110",
"10951",
"10952",
"10871",
"2439",
"2438",
"11022",
"11021",
"2742",
"2741",
"15552",
"8393",
"10950",
"2739",
"10817",
"2884",
"2753",
"9498",
"1330",
"2588",
"10430",
"10869",
"1008",
"10998",
"7287",
"10172",
"10171",
"10718",
"1001",
"1000",
"2557",
];
const test = async (userid) => {
let lst = await getBJ.getBJ(userid);
let return_lst = [];
......@@ -217,7 +8,7 @@ const test = async (userid) => {
return_lst.push(lst[i].problem_number);
}
var stringJson = JSON.stringify(return_lst) + "\n";
var stringJson = JSON.stringify(lst) + "\n";
fs.open("test.json", "a", "666", function (err, id) {
if (err) {
console.log("file open err!!");
......@@ -232,4 +23,4 @@ const test = async (userid) => {
/*
*/
test("jwseo001");
test("thak00");
......
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.