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", }, }, }));