diff --git a/src/App.jsx b/src/App.jsx
index 1740908..f13aef6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,16 +1,17 @@
-import React from 'react';
-import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
-import { getToken } from '@utils/api';
-import './scss/main.scss';
-import Footer from './components/Footer';
+import React from "react";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import { getToken } from "@utils/api";
+import "./scss/main.scss";
+import Footer from "./components/Footer";
-import Timetable from './views/timetable';
-import Login, { LoginBox } from './views/login/index';
-import PrivacyPolicy from './views/terms/PrivacyPolicy';
-import TermsOfService from './views/terms/TermsOfService';
-import Admin from './views/admin';
-import usePopup from './components/usePopup';
-import Notice from './views/notice';
+import Timetable from "./views/timetable";
+import Login, { LoginBox } from "./views/login/index";
+import PrivacyPolicy from "./views/terms/PrivacyPolicy";
+import TermsOfService from "./views/terms/TermsOfService";
+import Admin from "./views/admin";
+import EmailAuth from "./views/EmailAuth";
+import usePopup from "./components/usePopup";
+import Notice from "./views/notice";
function ErrorPage() {
return
404 Not Found
;
@@ -31,6 +32,7 @@ function App() {
+
diff --git a/src/utils/api.js b/src/utils/api.js
index e7b443c..af0d8a2 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -1,28 +1,30 @@
/* eslint-disable no-console */
/* eslint-disable no-unused-vars */
-import axios from 'axios';
-import usePopup from '@components/usePopup';
+import axios from "axios";
+import usePopup from "@components/usePopup";
const { API_URL_BASE } = process.env;
-const makeConfig = ({ method, url }, needToken = true) => (initialData = {}) => {
+const makeConfig = ({ method, url }, needToken = true) => (
+ initialData = {}
+) => {
const config = {
method,
url,
needToken,
setPath: (...path) => {
- config.url += ['', ...path].join('/');
+ config.url += ["", ...path].join("/");
return config;
},
- setQuery: query => {
+ setQuery: (query) => {
config.params = query;
return config;
},
- setData: data => {
- if (method === 'GET') {
+ setData: (data) => {
+ if (method === "GET") {
config.setQuery(data);
} else {
config.data = data;
@@ -35,34 +37,36 @@ const makeConfig = ({ method, url }, needToken = true) => (initialData = {}) =>
};
// API CONFIG OBJECT
-const GET = url => ({ method: 'GET', url });
-const POST = url => ({ method: 'POST', url });
-const PUT = url => ({ method: 'PUT', url });
-const PATCH = url => ({ method: 'PATCH', url });
-const DELETE = url => ({ method: 'DELETE', url });
+const GET = (url) => ({ method: "GET", url });
+const POST = (url) => ({ method: "POST", url });
+const PUT = (url) => ({ method: "PUT", url });
+const PATCH = (url) => ({ method: "PATCH", url });
+const DELETE = (url) => ({ method: "DELETE", url });
// API CONFIG LIST
-export const API_LOGIN = makeConfig(POST('/user/login'), false);
-export const API_GET_SEMESTER = makeConfig(GET('/semester'));
-export const API_GET_SEMESTERS = makeConfig(GET('/semesters'));
-export const API_GET_NOTICE = makeConfig(GET('/notice'));
-export const API_GET_USE_NOTICE = makeConfig(GET('/notice/use'));
-export const API_GET_HOT_NOTICE = makeConfig(GET('/notice/hot'));
-export const API_CREATE_NOTICE = makeConfig(POST('/notice'));
-export const API_UPDATE_NOTICE = makeConfig(PATCH('/notice'));
-export const API_DELETE_NOTICE = makeConfig(DELETE('/notice'));
-export const API_GET_ALL_LECTURES = makeConfig(GET('/lecture'));
-export const API_UPDATE_LECTURES = makeConfig(PATCH('/lecture'));
-export const API_DELETE_TLECTURE = makeConfig(DELETE('/timetable/tlecture'));
-export const API_ADD_TLECTURE = makeConfig(POST('/timetable/tlecture'));
-export const API_CREATE_TIMETABLE = makeConfig(POST('/timetable'));
-export const API_DELETE_TIMETABLE = makeConfig(DELETE('/timetable'));
-export const API_GET_TIMETABLES = makeConfig(GET('/timetable'));
-export const API_PATCH_TIMETABLE_NAME = makeConfig(PATCH('/timetable/name'));
-export const API_GET_HISTORIES = makeConfig(GET('/history'));
-export const API_SIGN_UP = makeConfig(POST('/user'), false);
-export const API_FIND_ID = makeConfig(GET('/user/id'), false);
-export const API_FIND_PW = makeConfig(GET('/user/password'), false);
+export const API_LOGIN = makeConfig(POST("/user/login"), false);
+export const API_GET_SEMESTER = makeConfig(GET("/semester"));
+export const API_GET_SEMESTERS = makeConfig(GET("/semesters"));
+export const API_GET_NOTICE = makeConfig(GET("/notice"));
+export const API_GET_USE_NOTICE = makeConfig(GET("/notice/use"));
+export const API_GET_HOT_NOTICE = makeConfig(GET("/notice/hot"));
+export const API_CREATE_NOTICE = makeConfig(POST("/notice"));
+export const API_UPDATE_NOTICE = makeConfig(PATCH("/notice"));
+export const API_DELETE_NOTICE = makeConfig(DELETE("/notice"));
+export const API_GET_ALL_LECTURES = makeConfig(GET("/lecture"));
+export const API_UPDATE_LECTURES = makeConfig(PATCH("/lecture"));
+export const API_DELETE_TLECTURE = makeConfig(DELETE("/timetable/tlecture"));
+export const API_ADD_TLECTURE = makeConfig(POST("/timetable/tlecture"));
+export const API_CREATE_TIMETABLE = makeConfig(POST("/timetable"));
+export const API_DELETE_TIMETABLE = makeConfig(DELETE("/timetable"));
+export const API_GET_TIMETABLES = makeConfig(GET("/timetable"));
+export const API_PATCH_TIMETABLE_NAME = makeConfig(PATCH("/timetable/name"));
+export const API_GET_HISTORIES = makeConfig(GET("/history"));
+export const API_SIGN_UP = makeConfig(POST("/user"), false);
+export const API_FIND_ID = makeConfig(GET("/user/id"), false);
+export const API_FIND_PW = makeConfig(GET("/user/password"), false);
+export const API_MAKE_AUTH_CODE = makeConfig(POST("/user/auth/code"));
+export const API_MATCH_AUTH_CODE = makeConfig(POST("/user/auth"));
const axiosInstance = axios.create({
baseURL: `${API_URL_BASE}/api`,
@@ -70,31 +74,31 @@ const axiosInstance = axios.create({
});
axiosInstance.interceptors.request.use(
- config => {
+ (config) => {
const token = getToken();
if (token) {
config.headers.Authorization = token;
}
return config;
},
- error => {
+ (error) => {
const [, showPopup] = usePopup();
console.error(error);
- showPopup('에러', '서버를 찾을 수 없어요...');
+ showPopup("에러", "서버를 찾을 수 없어요...");
return null;
- },
+ }
);
axiosInstance.interceptors.response.use(
- config => config,
- error => Promise.resolve(error.response),
+ (config) => config,
+ (error) => Promise.resolve(error.response)
);
export async function requestAPI(config) {
try {
if (config.needToken && !getToken()) {
// token required but not found: API wouldn't be requested
- window.location.href = '/login';
+ window.location.href = "/login";
return null;
}
@@ -114,25 +118,25 @@ export async function requestAPI(config) {
}
export function setToken(token) {
- localStorage.setItem('token', token);
+ localStorage.setItem("token", token);
}
export function getToken() {
- return localStorage.getItem('token');
+ return localStorage.getItem("token");
}
export function removeToken() {
- localStorage.removeItem('token');
+ localStorage.removeItem("token");
}
export function setUserID(userID) {
- localStorage.setItem('userID', userID);
+ localStorage.setItem("userID", userID);
}
export function getUserID() {
- return localStorage.getItem('userID');
+ return localStorage.getItem("userID");
}
export function removeUserID() {
- localStorage.removeItem('userID');
+ localStorage.removeItem("userID");
}
diff --git a/src/views/EmailAuth.jsx b/src/views/EmailAuth.jsx
new file mode 100644
index 0000000..d9e0f34
--- /dev/null
+++ b/src/views/EmailAuth.jsx
@@ -0,0 +1,145 @@
+import React, { useEffect, useState } from "react";
+import StatusCodes from "http-status-codes";
+import {
+ requestAPI,
+ API_MAKE_AUTH_CODE,
+ API_MATCH_AUTH_CODE,
+} from "@utils/api";
+import {
+ Button,
+ Box,
+ makeStyles,
+ Container,
+ Typography,
+} from "@material-ui/core";
+import UosInput from "@components/UosInput";
+
+export default function EmailAuth() {
+ const [userEmail, setUserEmail] = useState(null);
+ const [isCodeSend, setIsCodeSend] = useState(false);
+ const [code, setCode] = useState(null);
+
+ const classes = useStyles();
+
+ const codeOnChange = (e) => {
+ setCode(e.target.value);
+ };
+
+ const emailOnChange = (e) => {
+ setUserEmail(e.target.value);
+ };
+
+ const codeOnEnterPress = (e) => {
+ if (e.key == "Enter") {
+ handleAuth();
+ }
+ };
+
+ const emailOnEnterPress = (e) => {
+ if (e.key == "Enter") {
+ handleSendCode();
+ }
+ };
+
+ const handleSendCode = async () => {
+ try {
+ const res = await requestAPI(API_MAKE_AUTH_CODE({ email: userEmail }));
+ if (res.status === StatusCodes.NO_CONTENT) setIsCodeSend(true);
+ } catch (err) {
+ alert(err.message);
+ throw err;
+ }
+ };
+
+ const handleAuth = async () => {
+ try {
+ const userId = window.localStorage.getItem("userID");
+ const res = await requestAPI(API_MATCH_AUTH_CODE().setPath(userId, code));
+ if (res.status === StatusCodes.NO_CONTENT) {
+ alert("이메일이 성공적으로 인증되었습니다!");
+ }
+ } catch (err) {
+ alert(err);
+ throw err;
+ }
+ };
+
+ return (
+
+ 이메일 인증
+
+ 서울시립대학교 포털 이메일 인증을 하신 후 강의 교환 서비스를 이용하실 수
+ 있습니다.
+
+
+
+
+
+ {isCodeSend && (
+
+
+
+
+ )}
+
+ );
+}
+
+const useStyles = makeStyles({
+ container: {
+ margin: "auto",
+ },
+ title: {
+ alignItems: "center",
+ textAlign: "center",
+ fontSize: "1.5rem",
+ fontWeight: "700",
+ marginBottom: "1rem",
+ },
+ subTitle: {
+ alignItems: "center",
+ textAlign: "center",
+ fontSize: "1rem",
+ marginButtom: "1rem",
+ color: "#A3A2A2",
+ },
+ inputWrapper: {
+ display: "flex",
+ width: "60%",
+ flexDirection: "row",
+ marginTop: "2rem",
+ },
+ input: {
+ flex: 9,
+ },
+ button: {
+ flex: 3,
+ marginLeft: "1rem",
+ backgroundColor: "#CFCFCF",
+ textAlign: "center",
+ boxShadow: "3px 4px 4px rgba(0, 0, 0, 0.25)",
+ borderRadius: "1.5rem",
+ color: "#FFF",
+ },
+});
diff --git a/src/views/timetable/ShareDialog.jsx b/src/views/timetable/ShareDialog.jsx
index d1002fc..fa84130 100644
--- a/src/views/timetable/ShareDialog.jsx
+++ b/src/views/timetable/ShareDialog.jsx
@@ -1,8 +1,14 @@
-import { toPng } from 'html-to-image';
-import React, { useEffect, useState } from 'react';
-import CustomDialog from '@components/CustomDialog';
-import { Box, Button, makeStyles, TextField, Typography } from '@material-ui/core';
-import { TimetableElementID } from './Timetable';
+import { toPng } from "html-to-image";
+import React, { useEffect, useState } from "react";
+import CustomDialog from "@components/CustomDialog";
+import {
+ Box,
+ Button,
+ makeStyles,
+ TextField,
+ Typography,
+} from "@material-ui/core";
+import { TimetableElementID } from "./Timetable";
export default function ShareDialog(props) {
// props
@@ -13,9 +19,10 @@ export default function ShareDialog(props) {
const classes = useStyles();
// states
- const [imageSrc, setImageSrc] = useState('');
+ const [imageSrc, setImageSrc] = useState("");
const [imageResolutionWidth, setImageResolutionWidth] = useState(screenWidth);
- const [imageResolutionHeight, setImageResolutionHeight] = useState(screenHeight);
+ const [imageResolutionHeight, setImageResolutionHeight] =
+ useState(screenHeight);
// TODO: use debounce
// generate timetable image
@@ -27,10 +34,14 @@ export default function ShareDialog(props) {
// get real size of timetable in pixel
// const beforeWidth = parseInt(window.getComputedStyle(timetableElement).width);
- const beforeHeight = parseInt(window.getComputedStyle(timetableElement).height);
+ const beforeHeight = parseInt(
+ window.getComputedStyle(timetableElement).height
+ );
// resize timetable (fix height)
- timetableElement.style.width = `${Math.floor(beforeHeight * (imageResolutionWidth / imageResolutionHeight))}px`;
+ timetableElement.style.width = `${Math.floor(
+ beforeHeight * (imageResolutionWidth / imageResolutionHeight)
+ )}px`;
// configure option for rendering
const option = {
@@ -42,7 +53,7 @@ export default function ShareDialog(props) {
const src = await toPng(timetableElement, option);
// revert timetable size
- timetableElement.style.width = '';
+ timetableElement.style.width = "";
setImageSrc(src);
}, [open, imageResolutionWidth, imageResolutionHeight]);
@@ -61,8 +72,12 @@ export default function ShareDialog(props) {
- 이미지 해상도 변경
- 기기 해상도: {screenWidth} * {screenHeight}
+
+ 이미지 해상도 변경
+
+
+ 기기 해상도: {screenWidth} * {screenHeight}
+
setImageResolutionWidth(e.target.value)}
+ onChange={(e) => setImageResolutionWidth(e.target.value)}
required
/>
setImageResolutionHeight(e.target.value)}
+ onChange={(e) => setImageResolutionHeight(e.target.value)}
required
/>
@@ -100,64 +115,64 @@ export default function ShareDialog(props) {
);
}
-const useStyles = makeStyles(theme => ({
+const useStyles = makeStyles((theme) => ({
root: {
- display: 'flex',
- alignItems: 'stretch',
- padding: '1em 0',
- [theme.breakpoints.down('sm')]: {
- flexDirection: 'column',
- maxHeight: '80vh',
+ display: "flex",
+ alignItems: "stretch",
+ padding: "1em 0",
+ [theme.breakpoints.down("sm")]: {
+ flexDirection: "column",
+ maxHeight: "80vh",
},
- [theme.breakpoints.up('md')]: {
- flexDirection: 'row',
+ [theme.breakpoints.up("md")]: {
+ flexDirection: "row",
},
},
image: {
- objectFit: 'contain',
- display: 'block',
- maxWidth: '100%',
- maxHeight: '100%',
- margin: 'auto',
- boxShadow: '0 0.5em 1em rgba(0, 0, 0, .2)',
- overflow: 'hidden',
- borderRadius: '0.5em',
- [theme.breakpoints.down('sm')]: {
- maxHeight: '40vh',
+ objectFit: "contain",
+ display: "block",
+ maxWidth: "100%",
+ maxHeight: "100%",
+ margin: "auto",
+ boxShadow: "0 0.5em 1em rgba(0, 0, 0, .2)",
+ overflow: "hidden",
+ borderRadius: "0.5em",
+ [theme.breakpoints.down("sm")]: {
+ maxHeight: "40vh",
},
- [theme.breakpoints.up('md')]: {
- maxHeight: '80vh',
- maxWidth: '60%',
+ [theme.breakpoints.up("md")]: {
+ maxHeight: "80vh",
+ maxWidth: "60%",
},
},
controlBox: {
- display: 'flex',
- flexDirection: 'column',
- [theme.breakpoints.down('sm')]: {
- width: '100%',
- marginTop: '3em',
+ display: "flex",
+ flexDirection: "column",
+ [theme.breakpoints.down("sm")]: {
+ width: "100%",
+ marginTop: "3em",
},
- [theme.breakpoints.up('md')]: {
- width: '40%',
- marginLeft: '1.5em',
+ [theme.breakpoints.up("md")]: {
+ width: "40%",
+ marginLeft: "1.5em",
},
},
inputBox: {
- display: 'flex',
- margin: '1em 0',
- gap: '1em',
- '& > *': {
- width: '50%',
+ display: "flex",
+ margin: "1em 0",
+ gap: "1em",
+ "& > *": {
+ width: "50%",
},
},
saveButton: {
- display: 'block',
- width: '100%',
- [theme.breakpoints.down('sm')]: {
- marginTop: '2em',
+ display: "block",
+ width: "100%",
+ [theme.breakpoints.down("sm")]: {
+ marginTop: "2em",
},
- [theme.breakpoints.up('md')]: {
- marginTop: 'auto',
+ [theme.breakpoints.up("md")]: {
+ marginTop: "auto",
},
},
}));