diff --git a/client/.eslintrc.js b/.eslintrc.js similarity index 100% rename from client/.eslintrc.js rename to .eslintrc.js diff --git a/client/.prettierrc.js b/.prettierrc.js similarity index 100% rename from client/.prettierrc.js rename to .prettierrc.js diff --git a/client/.storybook/main.js b/.storybook/main.js similarity index 100% rename from client/.storybook/main.js rename to .storybook/main.js diff --git a/client/.storybook/preview.js b/.storybook/preview.js similarity index 100% rename from client/.storybook/preview.js rename to .storybook/preview.js diff --git a/.travis.yml b/.travis.yml index 5793061..244220d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,11 @@ before_install: branches: only: - main + - develop jobs: include: - stage: test script: - - cd client - yarn install - CI=false yarn run test diff --git a/README.md b/README.md index ad61a68..14e65c1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # hanpyo The timetable manager with course reviews at Koreatech + +### redirect +[Link](https://github.com/wooyeon-dev/hanpyo_fe) diff --git a/client/README.md b/client/README.md deleted file mode 100644 index b87cb00..0000000 --- a/client/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/client/src/api.d.ts b/client/src/api.d.ts deleted file mode 100644 index 4235123..0000000 --- a/client/src/api.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -// ==================================================== -// GraphQL query operation: HelloQuery -// ==================================================== - -export interface HelloQuery_hello { - __typename: "Hello"; - message: string; -} - -export interface HelloQuery { - hello: HelloQuery_hello | null; -} - -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -//============================================================== -// START Enums and Input Objects -//============================================================== - -//============================================================== -// END Enums and Input Objects -//============================================================== diff --git a/client/src/apollo.ts b/client/src/apollo.ts deleted file mode 100644 index 76bd8c7..0000000 --- a/client/src/apollo.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApolloClient, HttpLink, split, InMemoryCache } from '@apollo/client'; -import { WebSocketLink } from '@apollo/client/link/ws'; -import { getMainDefinition } from '@apollo/client/utilities'; -import { OperationDefinitionNode } from 'graphql'; - -// graphql api 주소 -const httpLink = new HttpLink({ - uri: 'http://localhost:4000/graphql', -}); - -// // subscription socket 통신 주소 -// const wsLink = new WebSocketLink({ -// uri: `ws://localhost:4000/graphql`, -// options: { -// reconnect: true, -// }, -// }); - -// 작성한 두 주소 병합 -const link = split( - ({ query }) => { - const { kind, operation } = getMainDefinition(query) as OperationDefinitionNode; - return kind === 'OperationDefinition' && operation === 'subscription'; - }, - // wsLink, - httpLink, -); - -// apollo client 생성 -const client = new ApolloClient({ - link, - cache: new InMemoryCache({}), -}); - -export default client; diff --git a/client/src/common/utils/index.ts b/client/src/common/utils/index.ts deleted file mode 100644 index a07865c..0000000 --- a/client/src/common/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as range } from './range'; -export * from './scroll'; -export { default as debounce } from './debounce'; diff --git a/client/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx b/client/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx deleted file mode 100644 index 5bc2ee6..0000000 --- a/client/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import Snackbar from '@material-ui/core/Snackbar'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import { useReactiveVar } from '@apollo/client'; -import { useStores } from '@/stores'; - -enum SnackbarType { - ADD_SUCCESS = 'ADD_SUCCESS', - DELETE_SUCCESS = 'DELETE_SUCCESS', -} - -const SNACKBAR_MESSAGE = { - [SnackbarType.ADD_SUCCESS]: '시간표가 추가되었습니다.', - [SnackbarType.DELETE_SUCCESS]: '시간표가 삭제되었습니다.', -}; - -const AlertSnackbar = (): JSX.Element => { - const { snackbarStore } = useStores(); - const snackbarState = useReactiveVar(snackbarStore.state.snackbarState); - const snackbarType = useReactiveVar(snackbarStore.state.snackbarType); - const onCloseHandler = (event: React.SyntheticEvent | React.MouseEvent, reason?: string) => { - if (reason === 'clickaway') { - return; - } - - snackbarStore.setSnackbarState(false); - }; - - return ( -
- - - - } - /> -
- ); -}; - -export { AlertSnackbar, SnackbarType }; diff --git a/client/src/components/UI/atoms/Button/Button.stories.tsx b/client/src/components/UI/atoms/Button/Button.stories.tsx deleted file mode 100644 index 4bf418f..0000000 --- a/client/src/components/UI/atoms/Button/Button.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { withKnobs } from '@storybook/addon-knobs'; -import { Story, Meta } from '@storybook/react/types-6-0'; -import { Button, ButtonProps, ButtonType } from '@/components/UI/atoms'; -import { action } from '@storybook/addon-actions'; - -export default { - title: 'atom/Button', - component: Button, - decorators: [withKnobs], -} as Meta; - -const Template: Story = (args) => - ); -}; - -export { StyledButton, ButtonType }; -export type { ButtonProps }; diff --git a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx b/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx deleted file mode 100644 index 2da0a8e..0000000 --- a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { useStores } from '@/stores'; -import { modalTypes } from '@/components/UI/organisms'; -import { HeaderAuthSectionArea } from './HeaderAuthSectionArea'; - -const HeaderAuthSection = (): JSX.Element => { - const { modalStore } = useStores(); - - const onLoginBtnClickListener = () => { - modalStore.changeModalState(modalTypes.LOGIN_MODAL, true); - }; - - const onSignUpBtnClickListener = () => { - modalStore.changeModalState(modalTypes.SIGN_UP_MODAL, true); - }; - - return ; -}; - -export { HeaderAuthSection }; diff --git a/client/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx b/client/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx deleted file mode 100644 index 0381c63..0000000 --- a/client/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { LectureBox, SameLectureBox } from '@/components/UI/atoms'; -import { useStores } from '@/stores'; -import { useReactiveVar } from '@apollo/client'; - -const useStyles = makeStyles((theme) => ({ - root: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - }, -})); - -const LectureBoxContainer = (): JSX.Element => { - const classes = useStyles(); - const { timeTableStore, lectureInfoStore } = useStores(); - const savedLectures = useReactiveVar(timeTableStore.state.selectedTabLectures); - const selectedTabIdx = useReactiveVar(timeTableStore.state.selectedTabIdx); - const nowSelectedLecture = useReactiveVar(lectureInfoStore.state.selectedLecture); - - const fillTableByLectures = () => { - if (selectedTabIdx === 0) return <>; - const lectureInfos = savedLectures[selectedTabIdx - 1]; - if (!lectureInfos) return <>; - return lectureInfos.map((elem) => { - if (typeof elem.time === 'string') return <>; - return elem.time.map((time) => { - return ; - }); - }); - }; - - const showSameLectures = () => { - const sameLectures = lectureInfoStore.getSameLectures(); - return sameLectures.map((sameLecture) => { - if (typeof sameLecture.time === 'string') return <>; - return sameLecture.time.map((time) => { - if (sameLecture.class === nowSelectedLecture?.class) return ; - return ; - }); - }); - }; - return ( -
- {fillTableByLectures()} - {showSameLectures()} -
- ); -}; - -export { LectureBoxContainer }; diff --git a/client/src/components/UI/molecules/LectureListBody/BasketLectureListBody.tsx b/client/src/components/UI/molecules/LectureListBody/BasketLectureListBody.tsx deleted file mode 100644 index bcbaa55..0000000 --- a/client/src/components/UI/molecules/LectureListBody/BasketLectureListBody.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { LectureInfos } from '@/components/UI/molecules'; -import { useReactiveVar } from '@apollo/client'; -import { useStores } from '@/stores'; - -interface BasketLectureListBodyProps { - isBasketList?: boolean; - getLectureInfos: (infos: Array) => any; -} - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - flexDirection: 'column', - marginLeft: '0.25rem', - width: '100%', - alignItems: 'center', - overflow: 'auto', - '&::-webkit-scrollbar': { - width: '0.3rem', - display: 'block', - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: theme.palette.grey[300], - borderRadius: '0.7rem', - }, - }, -})); - -const BasketLectureListBody = ({ isBasketList = false, getLectureInfos }: BasketLectureListBodyProps): JSX.Element => { - const classes = useStyles({ isBasketList }); - const { timeTableStore } = useStores(); - const savedLectures = useReactiveVar(timeTableStore.state.selectedTabLectures); - const selectedTabIdx = useReactiveVar(timeTableStore.state.selectedTabIdx); - const savedLecturesInSelectedTab = savedLectures[selectedTabIdx - 1]; - - return
{getLectureInfos(savedLecturesInSelectedTab)}
; -}; - -export { BasketLectureListBody }; diff --git a/client/src/components/UI/molecules/LectureListBody/SearchedLectureListBody.tsx b/client/src/components/UI/molecules/LectureListBody/SearchedLectureListBody.tsx deleted file mode 100644 index 8ff1053..0000000 --- a/client/src/components/UI/molecules/LectureListBody/SearchedLectureListBody.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { LectureInfos } from '@/components/UI/molecules'; -import { useStores } from '@/stores'; - -interface SearchedLectureListBodyProps { - isBasketList?: boolean; - getLectureInfos: (infos: Array) => any; -} - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - flexDirection: 'column', - marginLeft: '0.25rem', - width: '100%', - alignItems: 'center', - overflow: 'auto', - '&::-webkit-scrollbar': { - width: '0.3rem', - display: 'block', - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: theme.palette.grey[300], - borderRadius: '0.7rem', - }, - }, -})); - -const SearchedLectureListBody = ({ isBasketList = false, getLectureInfos }: SearchedLectureListBodyProps): JSX.Element => { - const classes = useStyles({ isBasketList }); - const { lectureInfoStore } = useStores(); - const lecturesData = lectureInfoStore.state.lectures(); - - return
{getLectureInfos(lecturesData)}
; -}; - -export { SearchedLectureListBody }; diff --git a/client/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx b/client/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx deleted file mode 100644 index d8705bb..0000000 --- a/client/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Thumb } from '@/components/UI/atoms'; - -interface LectureReviewThumbsProps { - upScore: number; - downScore: number; -} - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - }, -})); - -const LectureReviewThumbs = ({ upScore, downScore }: LectureReviewThumbsProps): JSX.Element => { - const classes = useStyles(); - return ( -
- - -
- ); -}; - -export { LectureReviewThumbs }; -export type { LectureReviewThumbsProps }; diff --git a/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx b/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx deleted file mode 100644 index c20b02d..0000000 --- a/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { LectureSearchFilter } from './LectureSearchFilter'; - -const majorSelectMenuProps = { - menuLabel: '개설학부', - menus: [ - { id: 0, title: '컴퓨터공학부', value: 0 }, - { id: 1, title: '디자인공학부', value: 1 }, - { id: 2, title: '기계공학부', value: 2 }, - { id: 3, title: '메카트로닉스공학부', value: 3 }, - { id: 4, title: '전기전자통신공학부', value: 4 }, - { id: 5, title: '에너지신소재화학공학부', value: 5 }, - { id: 6, title: '산업경영학부', value: 6 }, - ], - onSelectMenuChange: () => { - console.log('학부선택'); - }, -}; - -const daySelectMenuProps = { - menuLabel: '요일', - menus: [ - { id: 0, title: '월', value: 0 }, - { id: 1, title: '화', value: 1 }, - { id: 2, title: '수', value: 2 }, - { id: 3, title: '목', value: 3 }, - { id: 4, title: '금', value: 4 }, - ], - onSelectMenuChange: () => { - console.log('요일선택'); - }, -}; - -const gradeSelectMenuProps = { - menuLabel: '학점', - menus: [ - { id: 0, title: '1학점', value: 0 }, - { id: 1, title: '2학점', value: 1 }, - { id: 2, title: '3학점', value: 3 }, - { id: 3, title: '4학점', value: 4 }, - ], - onSelectMenuChange: () => { - console.log('학점선택'); - }, -}; - -const timeSelectMenuProps = { - menuLabel: '시간', - onSelectMenuChange: () => { - console.log('시간선택'); - }, -}; - -const LectureSearchFilterMenu = (): JSX.Element => { - return ( - - ); -}; - -export { LectureSearchFilterMenu }; diff --git a/client/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx b/client/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx deleted file mode 100644 index e80a1ee..0000000 --- a/client/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Button, DialogTitle, DialogContent, DialogActions, TextField } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { debounce } from '@/common/utils'; - -enum LoginModalType { - LOGIN_MODAL = 'LOGIN_MODAL', -} - -interface LoginModalContentProps { - onModalClose: () => void; -} - -const useStyles = makeStyles((theme) => ({ - title: { - display: 'flex', - justifyContent: 'center', - fontSize: '1.7rem', - color: theme.palette.primary.main, - }, -})); - -const LoginModalContent = ({ onModalClose }: LoginModalContentProps): JSX.Element => { - const classes = useStyles(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isValidEmail, setIsValidEmail] = useState(true); - const [isValidPassword, setIsValidPassword] = useState(true); - - const onDebouncedEmailChangeListener = debounce((e: React.ChangeEvent) => { - setEmail(e.target.value); - if ((e.target.value.length !== 0 && e.target.value.length < 8) || e.target.value.length > 12) setIsValidEmail(false); - else setIsValidEmail(true); - }, 500); - - const onDebouncedPasswordChangeListener = debounce((e: React.ChangeEvent) => { - setPassword(e.target.value); - if ((e.target.value.length !== 0 && e.target.value.length < 8) || e.target.value.length > 12) setIsValidPassword(false); - else setIsValidPassword(true); - }, 500); - - return ( - <> - - 한표 로그인 - - - - - - - - - - ); -}; - -export { LoginModalContent, LoginModalType }; -export type { LoginModalContentProps }; diff --git a/client/src/components/UI/molecules/SearchBar/SearchBar.tsx b/client/src/components/UI/molecules/SearchBar/SearchBar.tsx deleted file mode 100644 index 9669b5e..0000000 --- a/client/src/components/UI/molecules/SearchBar/SearchBar.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { InputBase } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { Search } from '@material-ui/icons'; - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - width: '100%', - boxSizing: 'border-box', - borderRadius: '10rem', - padding: `0.25rem 1.5rem`, - margin: '1.2rem 0 0 0', - backgroundColor: `${theme.palette.grey[100]}`, - alignItems: 'center', - justifyContent: 'space-between', - }, - input: { - width: '85%', - }, - icon: { - color: `${theme.palette.grey[500]}`, - }, -})); - -const SearchBar = (): JSX.Element => { - const classes = useStyles(); - - return ( -
- - - - ); -}; - -export { SearchBar }; diff --git a/client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx b/client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx deleted file mode 100644 index 26d017b..0000000 --- a/client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Button, DialogTitle, DialogContent, DialogActions, TextField } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { debounce } from '@/common/utils'; - -enum SignUpModalType { - SIGN_UP_MODAL = 'SIGN_UP_MODAL', -} - -interface SignUpModalContentProps { - onModalClose: () => void; -} - -const useStyles = makeStyles((theme) => ({ - title: { - display: 'flex', - justifyContent: 'center', - fontSize: '1.7rem', - color: theme.palette.primary.main, - }, -})); - -const SignUpModalContent = ({ onModalClose }: SignUpModalContentProps): JSX.Element => { - const classes = useStyles(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isValidEmail, setIsValidEmail] = useState(true); - const [isValidPassword, setIsValidPassword] = useState(true); - - const onDebouncedEmailChangeListener = debounce((e: React.ChangeEvent) => { - setEmail(e.target.value); - if ((e.target.value.length !== 0 && e.target.value.length < 8) || e.target.value.length > 12) setIsValidEmail(false); - else setIsValidEmail(true); - }, 500); - - const onDebouncedPasswordChangeListener = debounce((e: React.ChangeEvent) => { - setPassword(e.target.value); - if ((e.target.value.length !== 0 && e.target.value.length < 8) || e.target.value.length > 12) setIsValidPassword(false); - else setIsValidPassword(true); - }, 500); - - return ( - <> - - 한표 회원가입 - - - - - - - - - - - - ); -}; - -export { SignUpModalContent, SignUpModalType }; -export type { SignUpModalContentProps }; diff --git a/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx b/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx deleted file mode 100644 index e267a72..0000000 --- a/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { TimeTableAddFormContent } from './TimeTableAddFormContent'; - -const daySelectMenuProps = { - menuLabel: '요일', - menus: [ - { id: 0, title: '월', value: 0 }, - { id: 1, title: '화', value: 1 }, - { id: 2, title: '수', value: 2 }, - { id: 3, title: '목', value: 3 }, - { id: 4, title: '금', value: 4 }, - ], - onSelectMenuChange: () => { - console.log('요일선택'); - }, -}; - -const timeSelectMenuProps = { - menuLabel: '시간', - onSelectMenuChange: () => { - console.log('시간선택'); - }, -}; - -const TimeTableAddForm = (): JSX.Element => { - const onTimeTableFormSubmitListener = () => { - alert('submit!'); - }; - - return ( - - ); -}; - -export { TimeTableAddForm }; diff --git a/client/src/components/UI/organisms/LectureList/LectureList.tsx b/client/src/components/UI/organisms/LectureList/LectureList.tsx deleted file mode 100644 index 31505f3..0000000 --- a/client/src/components/UI/organisms/LectureList/LectureList.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { SnackbarType } from '@/components/UI/atoms'; -import { LectureInfo, LectureInfos, BasketLectureListBody, SearchedLectureListBody } from '@/components/UI/molecules'; -import { useStores } from '@/stores'; - -interface LectureListProps { - isBasketList?: boolean; -} - -interface CSSProps { - isBasketList?: boolean; -} - -const useStyles = makeStyles((theme) => ({ - rootWrapper: { - width: '35rem', - height: (props: CSSProps) => (props.isBasketList ? '12rem' : '19rem'), - margin: '1.2rem 0 0 0', - padding: '0 0.2rem 0.4rem 0.2rem', - boxSizing: 'border-box', - border: `1px solid ${theme.palette.grey[400]}`, - borderRadius: '0.7rem', - }, - root: { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - boxSizing: 'border-box', - alignItems: 'center', - }, -})); - -const headerInfos = { - code: '코드', - name: '강의명', - class: '분반', - prof: '교수님', - grade: '대상', - personnel: '정원', - dept: '개설학부', - time: '시간', -}; - -const LectureList = ({ isBasketList = false }: LectureListProps): JSX.Element => { - const classes = useStyles({ isBasketList }); - - const { timeTableStore, snackbarStore, lectureInfoStore } = useStores(); - const onLectureSearchDoubleClickListener = (lectureInfos: LectureInfos) => { - if (typeof lectureInfos.time === 'string') return; - timeTableStore.addLectureToTable(lectureInfos); - snackbarStore.setSnackbarType(SnackbarType.ADD_SUCCESS); - snackbarStore.setSnackbarState(true); - }; - const onBasketLectureDoubleClickListener = (lectureInfos: LectureInfos) => { - if (typeof lectureInfos.time === 'string') return; - timeTableStore.removeLectureFromTable(lectureInfos.name); - snackbarStore.setSnackbarType(SnackbarType.DELETE_SUCCESS); - snackbarStore.setSnackbarState(true); - }; - const onLectureSearchClickListener = (lectureInfos: LectureInfos) => { - if (typeof lectureInfos.time === 'string') return; - lectureInfoStore.state.selectedLecture(lectureInfos); - }; - const onBasketLectureClickListener = (lectureInfos: LectureInfos) => { - if (typeof lectureInfos.time === 'string') return; - lectureInfoStore.state.basketSelectedLecture(lectureInfos); - }; - const getLectureInfos = (infos: Array) => { - if (!infos) return <>; - return infos.map((elem: LectureInfos) => { - return ( - - ); - }); - }; - - const getLectureBody = () => { - return isBasketList ? ( - - ) : ( - - ); - }; - - return ( -
-
- {}} onClick={() => {}} /> - {getLectureBody()} -
-
- ); -}; - -export { LectureList }; -export type { LectureListProps }; diff --git a/client/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx b/client/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx deleted file mode 100644 index ff1969f..0000000 --- a/client/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { withKnobs } from '@storybook/addon-knobs'; -import { Story, Meta } from '@storybook/react/types-6-0'; -import { withStoryBox } from '@/components/HOC'; -import { LectureReview, LectureReviewProps } from './LectureReview'; - -export default { - title: 'organisms/LectureReview', - component: LectureReview, - decorators: [withKnobs], -} as Meta; - -const Template: Story = (args) => { - const LectureReviewStory = withStoryBox(args, 800)(LectureReview); - return ; -}; - -export const MyReview = Template.bind({}); -MyReview.args = { - infos: { - lectureName: '취준하며놀고먹기', - profName: '진혀쿠', - rating: 1.5, - period: '2021년도 1학기', - }, - content: '취준하면서 놀고 먹고 싶다구요? 저처럼 공부를 안 하면 된답니다!', - tags: ['백수', '진혀쿠', '나처럼살지마'], - scores: { - upScore: 5, - downScore: 18, - }, - isMine: true, -}; - -export const OthersReview = Template.bind({}); -OthersReview.args = { - infos: { - lectureName: '멋지게취업하기', - profName: '갓우진', - rating: 5, - period: '2021년도 1학기', - }, - content: '나는야 갓우진 그린팩토리를 점령할 사람이지!', - tags: ['취뽀', '신과함께'], - scores: { - upScore: 1000, - downScore: 0, - }, -}; diff --git a/client/src/components/UI/organisms/ModalPopup/LoginModalPopup.tsx b/client/src/components/UI/organisms/ModalPopup/LoginModalPopup.tsx deleted file mode 100644 index bec6f37..0000000 --- a/client/src/components/UI/organisms/ModalPopup/LoginModalPopup.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { ModalPopupArea, LoginModalContent } from '@/components/UI/molecules'; - -interface LoginModalPopupProps { - modalOpen: boolean; - onModalAreaClose: () => void; - onModalBtnClick: () => void; -} - -const LoginModalPopup = ({ modalOpen, onModalAreaClose, onModalBtnClick }: LoginModalPopupProps): JSX.Element => { - return ( - - - - ); -}; - -export { LoginModalPopup }; -export type { LoginModalPopupProps }; diff --git a/client/src/components/UI/organisms/ModalPopup/SignUpModalPopup.tsx b/client/src/components/UI/organisms/ModalPopup/SignUpModalPopup.tsx deleted file mode 100644 index 1c1b3ec..0000000 --- a/client/src/components/UI/organisms/ModalPopup/SignUpModalPopup.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { ModalPopupArea, SignUpModalContent } from '@/components/UI/molecules'; - -interface SignUpModalPopupProps { - modalOpen: boolean; - onModalAreaClose: () => void; - onModalBtnClick: () => void; -} - -const SignUpModalPopup = ({ modalOpen, onModalAreaClose, onModalBtnClick }: SignUpModalPopupProps): JSX.Element => { - return ( - - - - ); -}; - -export { SignUpModalPopup }; -export type { SignUpModalPopupProps }; diff --git a/client/src/components/UI/organisms/index.ts b/client/src/components/UI/organisms/index.ts deleted file mode 100644 index dda9c6f..0000000 --- a/client/src/components/UI/organisms/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { ModalPopup, modalTypes } from './ModalPopup/ModalPopup'; -export type { ModalType } from './ModalPopup/ModalPopup'; -export { LectureList } from './LectureList/LectureList'; -export type { LectureListProps } from './LectureList/LectureList'; -export { Header } from './Header/Header'; -export { TimeTableMenu } from './TimeTableMenu/TimeTableMenu'; -export { LectureReviewContainer } from './LectureReview/LectureReviewContainer'; diff --git a/client/src/components/pages/MyPage.tsx b/client/src/components/pages/MyPage.tsx deleted file mode 100644 index f497d8a..0000000 --- a/client/src/components/pages/MyPage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { Header } from '@/components/UI/organisms'; - -const MyPage = (): JSX.Element => { - return ( - <> -
-
마이페이지
- - ); -}; - -export default MyPage; diff --git a/client/src/components/pages/ReviewPage.tsx b/client/src/components/pages/ReviewPage.tsx deleted file mode 100644 index 74d5f54..0000000 --- a/client/src/components/pages/ReviewPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable react/no-array-index-key */ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Header, LectureReviewContainer } from '@/components/UI/organisms'; - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - }, -})); - -const ReviewPage = (): JSX.Element => { - const classes = useStyles(); - return ( -
-
- -
- ); -}; - -export default ReviewPage; diff --git a/client/src/queries/hello.queries.ts b/client/src/queries/hello.queries.ts deleted file mode 100644 index 8d911ab..0000000 --- a/client/src/queries/hello.queries.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const HELLO_QUERY = gql` - query HelloQuery { - hello { - message - } - } -`; diff --git a/client/src/router/Router.tsx b/client/src/router/Router.tsx deleted file mode 100644 index e3ccef9..0000000 --- a/client/src/router/Router.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; -import { MainPage, ReviewPage, MyPage } from '../components/pages'; - -function Router(): JSX.Element { - return ( - - - - - - ); -} - -export default Router; diff --git a/client/src/stores/LectureInfoStore.ts b/client/src/stores/LectureInfoStore.ts deleted file mode 100644 index d6f681e..0000000 --- a/client/src/stores/LectureInfoStore.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { makeVar, ReactiveVar } from '@apollo/client'; -import { RootStore } from '@/stores'; -import { LectureInfos } from '@/components/UI/molecules'; - -interface LectureInfoStoreState { - lectures: ReactiveVar; - selectedLecture: ReactiveVar; - basketSelectedLecture: ReactiveVar; -} - -class LectureInfoStore { - rootStore: RootStore; - - state: LectureInfoStoreState; - - constructor(rootStore: RootStore) { - this.rootStore = rootStore; - this.state = { - lectures: makeVar(testData), - selectedLecture: makeVar(null), - basketSelectedLecture: makeVar(null), - }; - } - - getSameLectures(): LectureInfos[] { - const { lectures, selectedLecture } = this.state; - if (selectedLecture() === null) return []; - return lectures().filter((lecture) => lecture.code === selectedLecture()?.code); - } -} - -const testData = [ - { - code: '111111', - name: '리눅스의 기초', - class: '01', - prof: '도눅스', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [ - { start: 3690, end: 3720 }, - { start: 540, end: 600 }, - ], - }, - { - code: '111112', - name: '신과함께', - class: '01', - prof: '갓우진', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 2340, end: 2460 }], - }, - { - code: '222222', - name: '백엔드 심화과정', - class: '01', - prof: '백마', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 5100, end: 5160 }], - }, - { - code: '333333', - name: '허접한 진혀쿠', - class: '01', - prof: '지녀쿠', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 5400, end: 5520 }], - }, - { - code: '444444', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: '555555', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: '666666', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: '777777', - name: '디자인', - class: '01', - prof: '정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYs', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYa', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYb', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYc', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'hanpyo', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'hanpyo', - name: '디자인커뮤니케이션', - class: '02', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [ - { start: 540, end: 600 }, - { start: 5220, end: 5340 }, - ], - }, - { - code: 'hanpyo', - name: '디자인커뮤니케이션', - class: '03', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 1980, end: 2040 }], - }, - { - code: 'HANPYg', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYh', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYi', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, - { - code: 'HANPYO', - name: '디자인커뮤니케이션', - class: '01', - prof: '윤정식', - grade: '03', - personnel: '25', - dept: '디자인건축공학부', - time: [{ start: 3660, end: 3780 }], - }, -]; - -export default LectureInfoStore; diff --git a/client/src/stores/SnackbarStore.ts b/client/src/stores/SnackbarStore.ts deleted file mode 100644 index 037ca5c..0000000 --- a/client/src/stores/SnackbarStore.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { makeVar, ReactiveVar } from '@apollo/client'; -import { RootStore } from '@/stores'; -import { SnackbarType } from '@/components/UI/atoms'; - -interface SnackbarStoreState { - snackbarState: ReactiveVar; - snackbarType: ReactiveVar; -} - -class SnackbarStore { - rootStore: RootStore; - - state: SnackbarStoreState; - - constructor(rootStore: RootStore) { - this.rootStore = rootStore; - this.state = { - snackbarState: makeVar(false), - snackbarType: makeVar(SnackbarType.ADD_SUCCESS), - }; - } - - setSnackbarState(newSnackbarState: boolean) { - const { snackbarState } = this.state; - - if (snackbarState() === newSnackbarState) return; - - snackbarState(newSnackbarState); - } - - setSnackbarType(newSnackbarType: SnackbarType) { - const { snackbarType } = this.state; - - if (snackbarType() === newSnackbarType) return; - - snackbarType(newSnackbarType); - } -} - -export default SnackbarStore; diff --git a/client/craco.config.js b/craco.config.js similarity index 100% rename from client/craco.config.js rename to craco.config.js diff --git a/client/package.json b/package.json similarity index 93% rename from client/package.json rename to package.json index 9afcb32..ce26c6a 100644 --- a/client/package.json +++ b/package.json @@ -40,14 +40,12 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "graphql": "^15.5.0", - "mobx": "^6.0.5", - "mobx-react-lite": "^3.1.7", + "http-proxy-middleware": "^1.3.1", "prettier": "^2.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", - "subscriptions-transport-ws": "^0.9.18", "typescript": "^4.1.3", "web-vitals": "^0.2.4" }, @@ -59,7 +57,7 @@ "eject": "react-scripts eject", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public", - "pretypes": "apollo schema:download --endpoint=http://localhost:4000/graphql", + "pretypes": "apollo schema:download --endpoint=https://hanpyo-server.herokuapp.com/graphql", "types": "apollo codegen:generate src/api.d.ts --queries=src/queries/*.queries.ts --addTypename --localSchemaFile=schema.json --target typescript --outputFlat" }, "eslintConfig": { diff --git a/client/public/favicon.ico b/public/favicon.ico similarity index 100% rename from client/public/favicon.ico rename to public/favicon.ico diff --git a/client/public/index.html b/public/index.html similarity index 96% rename from client/public/index.html rename to public/index.html index aa069f2..189cc99 100644 --- a/client/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + 한표 :: 한기대 시간표 웹 diff --git a/client/public/manifest.json b/public/manifest.json similarity index 100% rename from client/public/manifest.json rename to public/manifest.json diff --git a/client/public/robots.txt b/public/robots.txt similarity index 100% rename from client/public/robots.txt rename to public/robots.txt diff --git a/client/schema.json b/schema.json similarity index 66% rename from client/schema.json rename to schema.json index 6390dc8..136124d 100644 --- a/client/schema.json +++ b/schema.json @@ -4,41 +4,15 @@ "queryType": { "name": "Query" }, - "mutationType": null, + "mutationType": { + "name": "Mutation" + }, "subscriptionType": null, "types": [ - { - "kind": "OBJECT", - "name": "Hello", - "description": null, - "specifiedByUrl": null, - "fields": [ - { - "name": "message", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "SCALAR", - "name": "String", - "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", "specifiedByUrl": null, "fields": null, "inputFields": null, @@ -47,33 +21,20 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "Query", - "description": null, + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", "specifiedByUrl": null, - "fields": [ - { - "name": "hello", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "Hello", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], + "fields": null, "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", - "name": "Boolean", - "description": "The `Boolean` scalar type represents `true` or `false`.", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", "specifiedByUrl": null, "fields": null, "inputFields": null, @@ -83,56 +44,52 @@ }, { "kind": "OBJECT", - "name": "__Schema", - "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "name": "Lecture", + "description": "", "specifiedByUrl": null, "fields": [ { - "name": "description", - "description": null, + "name": "id", + "description": "", "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "types", - "description": "A list of all types supported by this server.", + "name": "code", + "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - } - } + "kind": "SCALAR", + "name": "String", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "queryType", - "description": "The type that query operations will be rooted at.", + "name": "name", + "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "String", "ofType": null } }, @@ -140,84 +97,60 @@ "deprecationReason": null }, { - "name": "mutationType", - "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "name": "room", + "description": "", "args": [], "type": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "subscriptionType", - "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "name": "professor", + "description": "", "args": [], "type": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "directives", - "description": "A list of all directives supported by this server.", + "name": "credit", + "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Directive", - "ofType": null - } - } + "kind": "SCALAR", + "name": "Int", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__Type", - "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", - "specifiedByUrl": null, - "fields": [ + }, { - "name": "kind", - "description": null, + "name": "requiredGrade", + "description": "", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "__TypeKind", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "name", - "description": null, + "name": "requiredMajor", + "description": "", "args": [], "type": { "kind": "SCALAR", @@ -228,179 +161,128 @@ "deprecationReason": null }, { - "name": "description", - "description": null, + "name": "totalStudentNumber", + "description": "", "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "specifiedByUrl", - "description": null, + "name": "currentStudentNumber", + "description": "", "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "fields", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false", - "isDeprecated": false, - "deprecationReason": null - } - ], + "name": "divisionNumber", + "description": "", + "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Field", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "interfaces", - "description": null, + "name": "department", + "description": "", "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "possibleTypes", - "description": null, + "name": "lectureTimes", + "description": "", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - } + "kind": "OBJECT", + "name": "LectureTime", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "LectureTime", + "description": "", + "specifiedByUrl": null, + "fields": [ { - "name": "enumValues", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false", - "isDeprecated": false, - "deprecationReason": null - } - ], + "name": "start", + "description": "", + "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__EnumValue", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "inputFields", - "description": null, - "args": [ - { - "name": "includeDeprecated", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false", - "isDeprecated": false, - "deprecationReason": null - } - ], + "name": "end", + "description": "", + "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "__InputValue", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "ofType", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "__Type", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null } ], "inputFields": null, @@ -409,146 +291,195 @@ "possibleTypes": null }, { - "kind": "ENUM", - "name": "__TypeKind", - "description": "An enum describing what kind of type a given `__Type` is.", + "kind": "OBJECT", + "name": "Member", + "description": "", "specifiedByUrl": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "SCALAR", - "description": "Indicates this type is a scalar.", - "isDeprecated": false, - "deprecationReason": null - }, + "fields": [ { - "name": "OBJECT", - "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "INTERFACE", - "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "name": "email", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "UNION", - "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "ENUM", - "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "name": "nickname", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "INPUT_OBJECT", - "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "name": "grade", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "LIST", - "description": "Indicates this type is a list. `ofType` is a valid field.", + "name": "major", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "NON_NULL", - "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "name": "role", + "description": "", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null } ], + "inputFields": null, + "interfaces": [], + "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", - "name": "__Field", - "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "name": "Mutation", + "description": "", "specifiedByUrl": null, "fields": [ { - "name": "name", - "description": null, - "args": [], + "name": "signUp", + "description": "", + "args": [ + { + "name": "input", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SignUpDto", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Member", "ofType": null } }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": "", + "specifiedByUrl": null, + "fields": [ { - "name": "description", - "description": null, + "name": "myMemberInfo", + "description": "", "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Member", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "args", - "description": null, + "name": "memberDuplicatedByEmail", + "description": "", "args": [ { - "name": "includeDeprecated", - "description": null, + "name": "email", + "description": "", "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": "false", - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__InputValue", + "kind": "SCALAR", + "name": "String", "ofType": null } - } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], + ], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "Boolean", "ofType": null } }, @@ -556,9 +487,26 @@ "deprecationReason": null }, { - "name": "isDeprecated", - "description": null, - "args": [], + "name": "memberDuplicatedByNickname", + "description": "", + "args": [ + { + "name": "nickname", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], "type": { "kind": "NON_NULL", "name": null, @@ -572,13 +520,21 @@ "deprecationReason": null }, { - "name": "deprecationReason", - "description": null, + "name": "lectureInfos", + "description": "", "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Lecture", + "ofType": null + } + } }, "isDeprecated": false, "deprecationReason": null @@ -590,15 +546,15 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "__InputValue", - "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "kind": "INPUT_OBJECT", + "name": "SignUpDto", + "description": "", "specifiedByUrl": null, - "fields": [ + "fields": null, + "inputFields": [ { - "name": "name", - "description": null, - "args": [], + "name": "email", + "description": "", "type": { "kind": "NON_NULL", "name": null, @@ -608,93 +564,45 @@ "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], + "name": "password", + "description": "", "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "__Type", + "kind": "SCALAR", + "name": "String", "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "defaultValue", - "description": "A GraphQL-formatted string representing the default value for this input value.", - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], + "name": "name", + "description": "", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "deprecationReason", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "__EnumValue", - "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", - "specifiedByUrl": null, - "fields": [ - { - "name": "name", - "description": null, - "args": [], + "name": "nickname", + "description": "", "type": { "kind": "NON_NULL", "name": null, @@ -704,52 +612,55 @@ "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "isDeprecated", - "description": null, - "args": [], + "name": "grade", + "description": "", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "Boolean", + "name": "Int", "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "deprecationReason", - "description": null, - "args": [], + "name": "major", + "description": "", "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null } ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "specifiedByUrl": null, + "fields": null, "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, @@ -982,12 +893,684 @@ } ], "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "specifiedByUrl": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "specifiedByUrl": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "specifiedByUrl": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "specifiedByUrl": null, + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "specifiedByUrl": null, + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "specifiedByUrl": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null } ], "directives": [ { "name": "include", - "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", "isRepeatable": false, "locations": [ "FIELD", @@ -1015,7 +1598,7 @@ }, { "name": "skip", - "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", "isRepeatable": false, "locations": [ "FIELD", @@ -1043,18 +1626,16 @@ }, { "name": "deprecated", - "description": "Marks an element of a GraphQL schema as no longer supported.", + "description": "Marks the field or enum value as deprecated", "isRepeatable": false, "locations": [ "FIELD_DEFINITION", - "ARGUMENT_DEFINITION", - "INPUT_FIELD_DEFINITION", "ENUM_VALUE" ], "args": [ { "name": "reason", - "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "description": "The reason for the deprecation", "type": { "kind": "SCALAR", "name": "String", diff --git a/client/src/.babelrc.js b/src/.babelrc.js similarity index 100% rename from client/src/.babelrc.js rename to src/.babelrc.js diff --git a/client/src/App.tsx b/src/App.tsx similarity index 100% rename from client/src/App.tsx rename to src/App.tsx diff --git a/client/src/__test__/common/utils/range.test.ts b/src/__test__/common/utils/range.test.ts similarity index 100% rename from client/src/__test__/common/utils/range.test.ts rename to src/__test__/common/utils/range.test.ts diff --git a/src/__test__/common/utils/typeCheck.test.ts b/src/__test__/common/utils/typeCheck.test.ts new file mode 100644 index 0000000..85310a0 --- /dev/null +++ b/src/__test__/common/utils/typeCheck.test.ts @@ -0,0 +1,169 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { isString, isNumber, isBoolean, isNull, isUndefined, isObject, isArray, isDate, isFunction } from '@/common/utils/typeCheck'; + +describe('TypeCheck Util Test', () => { + const TEST_CASE = { + stringCase: 'string', + numberCase: 0, + booleanCase: true, + nullCase: null, + undefindedCase: undefined, + objectCase: {}, + arrayCase: [], + dateCase: new Date(), + functionCase: () => {}, + }; + + describe('isString() test', () => { + test('인자가 string 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isString(TEST_CASE.stringCase)).toEqual(true); + expect(isString(TEST_CASE.numberCase)).toEqual(false); + expect(isString(TEST_CASE.booleanCase)).toEqual(false); + expect(isString(TEST_CASE.nullCase)).toEqual(false); + expect(isString(TEST_CASE.undefindedCase)).toEqual(false); + expect(isString(TEST_CASE.objectCase)).toEqual(false); + expect(isString(TEST_CASE.arrayCase)).toEqual(false); + expect(isString(TEST_CASE.dateCase)).toEqual(false); + expect(isString(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isNumber() test', () => { + test('인자가 number 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isNumber(TEST_CASE.numberCase)).toEqual(true); + expect(isNumber(TEST_CASE.stringCase)).toEqual(false); + expect(isNumber(TEST_CASE.booleanCase)).toEqual(false); + expect(isNumber(TEST_CASE.nullCase)).toEqual(false); + expect(isNumber(TEST_CASE.undefindedCase)).toEqual(false); + expect(isNumber(TEST_CASE.objectCase)).toEqual(false); + expect(isNumber(TEST_CASE.arrayCase)).toEqual(false); + expect(isNumber(TEST_CASE.dateCase)).toEqual(false); + expect(isNumber(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isBoolean() test', () => { + test('인자가 boolean 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isBoolean(TEST_CASE.booleanCase)).toEqual(true); + expect(isBoolean(TEST_CASE.numberCase)).toEqual(false); + expect(isBoolean(TEST_CASE.stringCase)).toEqual(false); + expect(isBoolean(TEST_CASE.nullCase)).toEqual(false); + expect(isBoolean(TEST_CASE.undefindedCase)).toEqual(false); + expect(isBoolean(TEST_CASE.objectCase)).toEqual(false); + expect(isBoolean(TEST_CASE.arrayCase)).toEqual(false); + expect(isBoolean(TEST_CASE.dateCase)).toEqual(false); + expect(isBoolean(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isNull() test', () => { + test('인자가 null 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isNull(TEST_CASE.nullCase)).toEqual(true); + expect(isNull(TEST_CASE.booleanCase)).toEqual(false); + expect(isNull(TEST_CASE.numberCase)).toEqual(false); + expect(isNull(TEST_CASE.stringCase)).toEqual(false); + expect(isNull(TEST_CASE.undefindedCase)).toEqual(false); + expect(isNull(TEST_CASE.objectCase)).toEqual(false); + expect(isNull(TEST_CASE.arrayCase)).toEqual(false); + expect(isNull(TEST_CASE.dateCase)).toEqual(false); + expect(isNull(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isUndefined() test', () => { + test('인자가 undefined 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isUndefined(TEST_CASE.undefindedCase)).toEqual(true); + expect(isUndefined(TEST_CASE.nullCase)).toEqual(false); + expect(isUndefined(TEST_CASE.booleanCase)).toEqual(false); + expect(isUndefined(TEST_CASE.numberCase)).toEqual(false); + expect(isUndefined(TEST_CASE.stringCase)).toEqual(false); + expect(isUndefined(TEST_CASE.objectCase)).toEqual(false); + expect(isUndefined(TEST_CASE.arrayCase)).toEqual(false); + expect(isUndefined(TEST_CASE.dateCase)).toEqual(false); + expect(isUndefined(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isObject() test', () => { + test('인자가 object 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isObject(TEST_CASE.objectCase)).toEqual(true); + expect(isObject(TEST_CASE.undefindedCase)).toEqual(false); + expect(isObject(TEST_CASE.nullCase)).toEqual(false); + expect(isObject(TEST_CASE.booleanCase)).toEqual(false); + expect(isObject(TEST_CASE.numberCase)).toEqual(false); + expect(isObject(TEST_CASE.stringCase)).toEqual(false); + expect(isObject(TEST_CASE.arrayCase)).toEqual(false); + expect(isObject(TEST_CASE.dateCase)).toEqual(false); + expect(isObject(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isArray() test', () => { + test('인자가 array 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isArray(TEST_CASE.arrayCase)).toEqual(true); + expect(isArray(TEST_CASE.objectCase)).toEqual(false); + expect(isArray(TEST_CASE.undefindedCase)).toEqual(false); + expect(isArray(TEST_CASE.nullCase)).toEqual(false); + expect(isArray(TEST_CASE.booleanCase)).toEqual(false); + expect(isArray(TEST_CASE.numberCase)).toEqual(false); + expect(isArray(TEST_CASE.stringCase)).toEqual(false); + expect(isArray(TEST_CASE.dateCase)).toEqual(false); + expect(isArray(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isDate() test', () => { + test('인자가 date 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isDate(TEST_CASE.dateCase)).toEqual(true); + expect(isDate(TEST_CASE.objectCase)).toEqual(false); + expect(isDate(TEST_CASE.undefindedCase)).toEqual(false); + expect(isDate(TEST_CASE.nullCase)).toEqual(false); + expect(isDate(TEST_CASE.booleanCase)).toEqual(false); + expect(isDate(TEST_CASE.numberCase)).toEqual(false); + expect(isDate(TEST_CASE.stringCase)).toEqual(false); + expect(isDate(TEST_CASE.arrayCase)).toEqual(false); + expect(isDate(TEST_CASE.functionCase)).toEqual(false); + }); + }); + + describe('isFunction() test', () => { + test('인자가 function 타입이면 true를, 아니면 false를 반환한다.', () => { + // given + // when + // then + expect(isFunction(TEST_CASE.functionCase)).toEqual(true); + expect(isFunction(TEST_CASE.dateCase)).toEqual(false); + expect(isFunction(TEST_CASE.objectCase)).toEqual(false); + expect(isFunction(TEST_CASE.undefindedCase)).toEqual(false); + expect(isFunction(TEST_CASE.nullCase)).toEqual(false); + expect(isFunction(TEST_CASE.booleanCase)).toEqual(false); + expect(isFunction(TEST_CASE.numberCase)).toEqual(false); + expect(isFunction(TEST_CASE.stringCase)).toEqual(false); + expect(isFunction(TEST_CASE.arrayCase)).toEqual(false); + }); + }); +}); diff --git a/src/__test__/common/utils/unit.test.ts b/src/__test__/common/utils/unit.test.ts new file mode 100644 index 0000000..4a18aab --- /dev/null +++ b/src/__test__/common/utils/unit.test.ts @@ -0,0 +1,28 @@ +import { toPixel, toRem } from '@/common/utils/unit'; + +describe('Unit Util Test', () => { + describe('toRem() test', () => { + test('Pixel 값을 Rem 값으로 변경할 수 있다.', () => { + // given + const pixel = 16; + + // when + const rem = toRem(pixel); + + // then + expect(rem).toEqual(1); + }); + }); + describe('toPixel() test', () => { + test('Rem 값을 Pixel 값으로 변경할 수 있다.', () => { + // given + const rem = 1; + + // when + const pixel = toPixel(rem); + + // then + expect(pixel).toEqual(16); + }); + }); +}); diff --git a/src/__test__/common/utils/validator.test.ts b/src/__test__/common/utils/validator.test.ts new file mode 100644 index 0000000..bf0b9b4 --- /dev/null +++ b/src/__test__/common/utils/validator.test.ts @@ -0,0 +1,131 @@ +import { isEmailID, isPassword, isName, isNickname, isGrade, isMajor } from '@/common/utils/validator'; + +describe('Validator Util Test', () => { + const checkTestCases = (testFunc: Function, testCases: string[], equalValue: boolean) => { + testCases.forEach((testCase) => { + expect(testFunc(testCase)).toEqual(equalValue); + }); + }; + + describe('isEmailID() test', () => { + test('이메일 아이디는 1자리 이상 12자리 이하여야한다.', () => { + // given + // when + const validCases = ['a', 'test12345', 'test', 'testtesttest']; + const inValidCases = ['', 'testtesttesttesttest']; + + // then + checkTestCases(isEmailID, validCases, true); + checkTestCases(isEmailID, inValidCases, false); + }); + + test('이메일 아이디는 영소문자, 숫자, _ 로만 조합된 문자열만 가능하다.', () => { + // given + // when + const validCases = ['abc', '1234', '_', 'abc123', '__abc123']; + const inValidCases = ['~!@#$%^&*()', 'test1234!', '@test1234', 'Te##!@st', '테스트', '테스트1234', '테스트Test1234', 'Test', 'ABCD']; + + // then + checkTestCases(isEmailID, validCases, true); + checkTestCases(isEmailID, inValidCases, false); + }); + }); + + describe('isPassword() test', () => { + test('패스워드는 8자리 이상 12자리 이하여야한다.', () => { + // given + // when + const validCases = ['test1234!', '123test!@#', '%%%1234test', 'TESTtest12!']; + const inValidCases = ['', 'test12!', 'testtest', 'test!@#$', '!@#!@#$', '12345678']; + + // then + checkTestCases(isPassword, validCases, true); + checkTestCases(isPassword, inValidCases, false); + }); + + test('패스워드는 영문, 특수문자, 숫자 모두 최소 1개 이상으로 조합된 문자열만 가능하다.', () => { + // given + // when + const validCases = ['testTEST12!@', '!@12testTEST', 'test123!@', '123test!@']; + const inValidCases = ['~!@#$%^&*()', '12341234', 'testtest', 'TESTTEST', '테스트테스트!!', '테스트Test1234', 'Test1234', 'test!@#$']; + + // then + checkTestCases(isPassword, validCases, true); + checkTestCases(isPassword, inValidCases, false); + }); + }); + + describe('isName() test', () => { + test('이름은 2자 이상이여야 한다.', () => { + // given + // when + const validCases = ['김훈', '홍길동', '모나리자']; + const inValidCases = ['', '김']; + + // then + checkTestCases(isName, validCases, true); + checkTestCases(isName, inValidCases, false); + }); + + test('이름은 한글로 조합된 문자열만 가능하다.', () => { + // given + // when + const validCases = ['김훈', '홍길동', '모나리자']; + const inValidCases = ['', 'name', 'NAME', '김name', '1234', '김1234', '!@#$', '김!@#$#']; + + // then + checkTestCases(isName, validCases, true); + checkTestCases(isName, inValidCases, false); + }); + }); + + describe('isNickname() test', () => { + test('닉네임은 1자 이상이어야한다.', () => { + // given + // when + const validCases = ['김훈', '홍길동', '모나리자']; + const inValidCases = ['']; + + // then + checkTestCases(isNickname, validCases, true); + checkTestCases(isNickname, inValidCases, false); + }); + + test('닉네임은 영문, 한글, 숫자로 조합된 문자열만 가능하다.', () => { + // given + // when + const validCases = ['김훈', 'name', 'NAME', '김name', '1234', '김1234']; + const inValidCases = ['', '!@#$', '김!@#$#']; + + // then + checkTestCases(isNickname, validCases, true); + checkTestCases(isNickname, inValidCases, false); + }); + }); + + describe('isGrade() test', () => { + test('학년은 1학년 ~ 4학년까지만 존재한다.', () => { + // given + // when + const validCases = ['1', '2', '3', '4']; + const inValidCases = ['', '12', '6', '8']; + + // then + checkTestCases(isGrade, validCases, true); + checkTestCases(isGrade, inValidCases, false); + }); + }); + + describe('isMajor() test', () => { + test('전공은 한글로 조합된 문자열만 가능하다.', () => { + // given + // when + const validCases = ['컴퓨터공학부', '산업경영학부', '기계공학부']; + const inValidCases = ['124', '', '!@#$', '김!@#$#']; + + // then + checkTestCases(isMajor, validCases, true); + checkTestCases(isMajor, inValidCases, false); + }); + }); +}); diff --git a/src/api.d.ts b/src/api.d.ts new file mode 100644 index 0000000..59f8327 --- /dev/null +++ b/src/api.d.ts @@ -0,0 +1,133 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetLectureInfos +// ==================================================== + +export interface GetLectureInfos_lectureInfos_lectureTimes { + __typename: "LectureTime"; + start: number; + end: number; +} + +export interface GetLectureInfos_lectureInfos { + __typename: "Lecture"; + id: string; + code: string; + name: string; + department: string; + room: string | null; + professor: string | null; + credit: number; + requiredGrade: number | null; + requiredMajor: string | null; + divisionNumber: number; + totalStudentNumber: number; + lectureTimes: (GetLectureInfos_lectureInfos_lectureTimes | null)[] | null; +} + +export interface GetLectureInfos { + lectureInfos: (GetLectureInfos_lectureInfos | null)[]; +} + +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetMemberDuplicatedByEmail +// ==================================================== + +export interface GetMemberDuplicatedByEmail { + memberDuplicatedByEmail: boolean; +} + +export interface GetMemberDuplicatedByEmailVariables { + email: string; +} + +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetMemberDuplicatedByNickname +// ==================================================== + +export interface GetMemberDuplicatedByNickname { + memberDuplicatedByNickname: boolean; +} + +export interface GetMemberDuplicatedByNicknameVariables { + nickname: string; +} + +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetMyMemberInfo +// ==================================================== + +export interface GetMyMemberInfo_myMemberInfo { + __typename: "Member"; + nickname: string | null; + major: string | null; +} + +export interface GetMyMemberInfo { + myMemberInfo: GetMyMemberInfo_myMemberInfo; +} + +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: SignUp +// ==================================================== + +export interface SignUp_signUp { + __typename: "Member"; + id: string | null; + email: string | null; + name: string | null; + nickname: string | null; + grade: number | null; + major: string | null; + role: string | null; +} + +export interface SignUp { + signUp: SignUp_signUp; +} + +export interface SignUpVariables { + email: string; + password: string; + name: string; + nickname: string; + grade: number; + major: string; +} + +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +//============================================================== +// START Enums and Input Objects +//============================================================== + +//============================================================== +// END Enums and Input Objects +//============================================================== diff --git a/src/apollo.ts b/src/apollo.ts new file mode 100644 index 0000000..62ced72 --- /dev/null +++ b/src/apollo.ts @@ -0,0 +1,13 @@ +import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; + +const link = new HttpLink({ + uri: '/graphql', + credentials: 'include', +}); + +const client = new ApolloClient({ + link, + cache: new InMemoryCache(), +}); + +export default client; diff --git a/src/assets/loading.gif b/src/assets/loading.gif new file mode 100644 index 0000000..e0a76e2 Binary files /dev/null and b/src/assets/loading.gif differ diff --git a/src/common/hooks/index.ts b/src/common/hooks/index.ts new file mode 100644 index 0000000..78082c3 --- /dev/null +++ b/src/common/hooks/index.ts @@ -0,0 +1,5 @@ +export { default as useFetchAsync } from './useFetchAsync'; +export { default as useInputForm } from './useInputForm'; +export { default as useReactiveVars } from './useReactiveVars'; +export { default as useTimeSelectMenu } from './useTimeSelectMenu'; +export { default as useSelectMenu } from './useSelectMenu'; diff --git a/src/common/hooks/useFetchAsync.ts b/src/common/hooks/useFetchAsync.ts new file mode 100644 index 0000000..aedabbf --- /dev/null +++ b/src/common/hooks/useFetchAsync.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from 'react'; + +interface FetchCallback { + onCompleted?: Function; + onError?: Function; +} + +interface FetchParams { + queryData?: {}; + bodyData?: {}; +} + +const INIT_REQUEST_OPTION = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, +}; + +function useFetchAsync( + url: string, + option: RequestInit = INIT_REQUEST_OPTION, + callback?: FetchCallback, +): [({ queryData, bodyData }: FetchParams) => Promise, boolean] { + const [loading, setLoading] = useState(true); + + const executeCallback = (response: Response, data: any, callbacks: FetchCallback) => { + const { onCompleted, onError } = callbacks; + + if (response.ok && onCompleted) { + onCompleted(data); + return; + } + + if (onError) { + onError(); + } + }; + + const concatURLParams = (urlStr: string, queryParams: {}): string => { + return `${urlStr}?${new URLSearchParams(queryParams)}`; + }; + + const fetchData = async ({ queryData, bodyData }: FetchParams) => { + const fetchUrl = queryData ? concatURLParams(url, queryData) : url; + const response = await fetch(fetchUrl, { ...option, body: JSON.stringify({ ...bodyData }) }); + const data = response.bodyUsed ? await response.json() : null; + + if (callback) { + executeCallback(response, data, callback); + } + + setLoading(false); + }; + + return [fetchData, loading]; +} + +export default useFetchAsync; diff --git a/src/common/hooks/useInputForm.ts b/src/common/hooks/useInputForm.ts new file mode 100644 index 0000000..2871379 --- /dev/null +++ b/src/common/hooks/useInputForm.ts @@ -0,0 +1,101 @@ +import React, { useMemo, useState } from 'react'; +import { debounce } from '@/common/utils'; +import { isEmailID, isPassword, isName, isNickname, isGrade, isMajor } from '@/common/utils/validator'; + +enum InputNameType { + email = 'email', + password = 'password', + name = 'name', + nickname = 'nickname', + grade = 'grade', + major = 'major', +} + +interface Option { + validation?: boolean; + timeout?: number; + callback?: Function; +} + +interface InputUtility { + reset: () => void; + isEmpty: boolean; + valids: boolean[]; + isValid: boolean; +} + +const INIT_OPTIONS = { + validation: false, + timeout: 500, +}; + +const ERRORS = { + VALID: new Error(`'name' does not exist on type 'InputNameType' `), +}; + +function useInputForm(initInputState: T, options: Option = INIT_OPTIONS): [T, () => void, InputUtility] { + const { validation, timeout, callback } = { ...INIT_OPTIONS, ...options }; + + const createValidState = () => { + if (validation) { + return Object.keys(initInputState).map(() => true); + } + return []; + }; + + const [inputs, setInputs] = useState(initInputState); + const [valids, setValids] = useState(createValidState()); + + const validateValue = (name: string, value: string): boolean => { + if (name === InputNameType.email) return isEmailID(value); + if (name === InputNameType.password) return isPassword(value); + if (name === InputNameType.name) return isName(value); + if (name === InputNameType.nickname) return isNickname(value); + if (name === InputNameType.grade) return isGrade(value); + if (name === InputNameType.major) return isMajor(value); + + throw ERRORS.VALID; + }; + + const onChangeListener = debounce((e: React.ChangeEvent) => { + const { value, name } = e.target; + + if (callback) { + callback(name); + } + + setInputs({ ...inputs, [name]: value }); + if (valids) { + const findIdx = Object.keys(inputs).findIndex((key) => key === name); + setValids( + valids.map((valid, idx) => { + if (idx === findIdx) return validateValue(name, value); + return valid; + }), + ); + } + }, timeout); + + const checkEmptyData = (inputState: T): boolean => { + return !Object.values(inputState).every((input) => !!input); + }; + + const checkValidData = (validState: boolean[]): boolean => { + return validState.every((valid) => valid); + }; + + const isEmpty = useMemo(() => checkEmptyData(inputs), [inputs]); + const isValid = useMemo(() => checkValidData(valids), [valids]); + + const reset = () => { + setInputs(initInputState); + + if (valids) { + setValids(createValidState()); + } + }; + + return [inputs, onChangeListener, { reset, isEmpty, valids, isValid }]; +} + +export default useInputForm; diff --git a/src/common/hooks/useReactiveVars.ts b/src/common/hooks/useReactiveVars.ts new file mode 100644 index 0000000..d38e356 --- /dev/null +++ b/src/common/hooks/useReactiveVars.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useReactiveVar } from '@apollo/client'; + +interface State { + [stateIndex: string]: any; +} + +function useReactiveVars(state: State): T { + return Object.keys(state).reduce((acc, cur) => { + return { ...acc, [cur]: useReactiveVar(state[cur]) }; + }, {}) as T; +} + +export default useReactiveVars; diff --git a/src/common/hooks/useSelectMenu.ts b/src/common/hooks/useSelectMenu.ts new file mode 100644 index 0000000..4cc46f8 --- /dev/null +++ b/src/common/hooks/useSelectMenu.ts @@ -0,0 +1,36 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from 'react'; + +interface Option { + callback: Function; +} + +interface SelectUtility { + reset: () => void; +} + +function useSelectMenu(initState: T, option?: Option): [T, (e: React.MouseEvent) => void, SelectUtility] { + const [selectState, setSelectState] = useState(initState); + + const onMenuClick = (e: React.MouseEvent) => { + const { dataset } = (e.target as HTMLElement).closest('li') as HTMLLIElement; + const { title, type } = dataset; + + if (type) { + setSelectState({ ...selectState, [type]: title ?? '' }); + } + + if (option?.callback) { + option.callback(type, title ?? ''); + } + }; + + const reset = () => { + setSelectState(initState); + }; + + return [selectState, onMenuClick, { reset }]; +} + +export default useSelectMenu; diff --git a/src/common/hooks/useTimeSelectMenu.ts b/src/common/hooks/useTimeSelectMenu.ts new file mode 100644 index 0000000..b48cc49 --- /dev/null +++ b/src/common/hooks/useTimeSelectMenu.ts @@ -0,0 +1,114 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useReducer, useMemo } from 'react'; + +interface TimeSelectState { + selectedAMPM: string | undefined; + selectedHour: string | undefined; + selectedMinute: string | undefined; + isSelect: boolean; +} + +enum TimeSelectValueType { + AM_PM = 'AM_PM', + HOUR = 'HOUR', + MINUTE = 'MINUTE', +} + +enum TimeSelectActionType { + SELECT_AM_PM = 'SELECT_AM_PM', + SELECT_HOUR = 'SELECT_HOUR', + SELECT_MINUTE = 'SELECT_MINUTE', + RESET = 'RESET', +} + +interface TimeSelectAction { + type: TimeSelectActionType; + value?: string; +} + +interface Option { + callback: Function; +} + +interface TimeSelectUtility { + time: number; + reset: () => void; +} + +const INIT_STATE: TimeSelectState = { + selectedAMPM: '오전', + selectedHour: '01', + selectedMinute: '00', + isSelect: false, +}; + +function reducer(prevState: TimeSelectState, action: TimeSelectAction): TimeSelectState { + switch (action.type) { + case TimeSelectActionType.SELECT_AM_PM: + return { ...prevState, selectedAMPM: action.value, isSelect: true }; + case TimeSelectActionType.SELECT_HOUR: + return { ...prevState, selectedHour: action.value, isSelect: true }; + case TimeSelectActionType.SELECT_MINUTE: + return { ...prevState, selectedMinute: action.value, isSelect: true }; + case TimeSelectActionType.RESET: + return INIT_STATE; + default: + return prevState; + } +} + +function useTimeSelectMenu( + option?: Option, +): [string, boolean, (e: React.MouseEvent) => void, (itemValue: string) => boolean, TimeSelectUtility] { + const [state, dispatch] = useReducer, TimeSelectState>(reducer, INIT_STATE, () => INIT_STATE); + const { selectedAMPM, selectedHour, selectedMinute, isSelect } = state; + + const value = `${selectedAMPM}${selectedHour && ` ${selectedHour} : `}${selectedMinute}`; + + const calculateTime = (): number => { + const ampm = selectedAMPM; + const hour = Number(selectedHour); + const minute = Number(selectedMinute); + + let time = ampm === '오후' ? 720 : 0; + time += hour * 60 + minute; + + return time; + }; + + const time = useMemo(calculateTime, [state]); + + const onMenuClick = (e: React.MouseEvent) => { + const { dataset } = (e.target as HTMLElement).closest('li') as HTMLLIElement; + const { type, title, selectType } = dataset; + + if (type === TimeSelectValueType.AM_PM) { + dispatch({ type: TimeSelectActionType.SELECT_AM_PM, value: title ?? '' }); + } + + if (type === TimeSelectValueType.HOUR) { + dispatch({ type: TimeSelectActionType.SELECT_HOUR, value: title ?? '' }); + } + + if (type === TimeSelectValueType.MINUTE) { + dispatch({ type: TimeSelectActionType.SELECT_MINUTE, value: title ?? '' }); + } + + if (option?.callback) { + option.callback(selectType, time); + } + }; + + const checkSelectedItem = (itemValue: string): boolean => { + return itemValue === selectedAMPM || itemValue === selectedHour || itemValue === selectedMinute; + }; + + const reset = () => { + dispatch({ type: TimeSelectActionType.RESET }); + }; + + return [value, isSelect, onMenuClick, checkSelectedItem, { time, reset }]; +} + +export default useTimeSelectMenu; diff --git a/client/src/common/styles/index.ts b/src/common/styles/index.ts similarity index 100% rename from client/src/common/styles/index.ts rename to src/common/styles/index.ts diff --git a/client/src/common/styles/theme.ts b/src/common/styles/theme.ts similarity index 92% rename from client/src/common/styles/theme.ts rename to src/common/styles/theme.ts index ce680a5..4fe50d8 100644 --- a/client/src/common/styles/theme.ts +++ b/src/common/styles/theme.ts @@ -16,10 +16,10 @@ const theme = createMuiTheme({ }, }, containedSecondary: { - backgroundColor: '#f5f5f5', + backgroundColor: '#f3f3f3', color: '#707070', '&:hover': { - backgroundColor: '#f5f5f5', + backgroundColor: '#f3f3f3', }, }, }, diff --git a/client/src/common/utils/debounce.ts b/src/common/utils/debounce.ts similarity index 87% rename from client/src/common/utils/debounce.ts rename to src/common/utils/debounce.ts index 0511881..e00046c 100644 --- a/client/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-this-alias */ const debounce = (func: Function, wait: number): (() => void) => { let timeout: NodeJS.Timeout | null; diff --git a/src/common/utils/getTimeBound.ts b/src/common/utils/getTimeBound.ts new file mode 100644 index 0000000..19b273f --- /dev/null +++ b/src/common/utils/getTimeBound.ts @@ -0,0 +1,11 @@ +const getTimeBoundByDay = (day: string) => { + if (day === '월') return { start: 0, end: 1440 }; + if (day === '화') return { start: 1440, end: 2880 }; + if (day === '수') return { start: 2880, end: 4320 }; + if (day === '목') return { start: 4320, end: 5760 }; + if (day === '금') return { start: 5760, end: 7200 }; + if (day === '토') return { start: 7200, end: 8640 }; + return { start: 0, end: 8640 }; +}; + +export { getTimeBoundByDay }; diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts new file mode 100644 index 0000000..a59fdda --- /dev/null +++ b/src/common/utils/index.ts @@ -0,0 +1,7 @@ +export { default as range } from './range'; +export * from './scroll'; +export { default as debounce } from './debounce'; +export * from './typeCheck'; +export * from './unit'; +export * from './getTimeBound'; +export { default as throttle } from './throttle'; diff --git a/client/src/common/utils/range.ts b/src/common/utils/range.ts similarity index 100% rename from client/src/common/utils/range.ts rename to src/common/utils/range.ts diff --git a/client/src/common/utils/scroll.ts b/src/common/utils/scroll.ts similarity index 100% rename from client/src/common/utils/scroll.ts rename to src/common/utils/scroll.ts diff --git a/src/common/utils/throttle.ts b/src/common/utils/throttle.ts new file mode 100644 index 0000000..873760e --- /dev/null +++ b/src/common/utils/throttle.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-this-alias */ +const throttle = (func: Function, wait: number): (() => void) => { + let timeout: NodeJS.Timeout | null = null; + + return (...args: any[]): void => { + const context = this; + if (!timeout) { + timeout = setTimeout(() => { + timeout = null; + func.apply(context, args); + }, wait); + } + }; +}; + +export default throttle; diff --git a/src/common/utils/typeCheck.ts b/src/common/utils/typeCheck.ts new file mode 100644 index 0000000..4c9c6ce --- /dev/null +++ b/src/common/utils/typeCheck.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +function getType(target: any): string { + return Object.prototype.toString.call(target).slice(8, -1); +} + +function isString(target: any): target is string { + return getType(target) === 'String'; +} + +function isNumber(target: any): target is number { + return getType(target) === 'Number'; +} + +function isBoolean(target: any): target is boolean { + return getType(target) === 'Boolean'; +} + +function isNull(target: any): target is null { + return getType(target) === 'Null'; +} + +function isUndefined(target: any): target is undefined { + return getType(target) === 'Undefined'; +} + +function isObject(target: any): target is object { + return getType(target) === 'Object'; +} + +function isArray(target: any): target is Array { + return getType(target) === 'Array'; +} + +function isDate(target: any): target is Date { + return getType(target) === 'Date'; +} + +function isFunction(target: any): target is Function { + return getType(target) === 'Function'; +} + +export { isString, isNumber, isBoolean, isNull, isUndefined, isObject, isArray, isDate, isFunction }; diff --git a/src/common/utils/unit.ts b/src/common/utils/unit.ts new file mode 100644 index 0000000..fd8232d --- /dev/null +++ b/src/common/utils/unit.ts @@ -0,0 +1,15 @@ +const DEFAULT_FONT_SIZE = 16; + +function toRem(pixel: number): number { + const rem = pixel / DEFAULT_FONT_SIZE; + + return rem; +} + +function toPixel(rem: number): number { + const pixel = rem * DEFAULT_FONT_SIZE; + + return pixel; +} + +export { toRem, toPixel }; diff --git a/src/common/utils/validator.ts b/src/common/utils/validator.ts new file mode 100644 index 0000000..517df6a --- /dev/null +++ b/src/common/utils/validator.ts @@ -0,0 +1,54 @@ +// 한표(한기대 포털 이메일) 아이디 제약조건 +// 영소문자, 숫자, _ 로만 조합된 문자열만 가능 +// 최소 1자리 이상 12자리 이하 + +// 한표 패스워드 제약조건 +// 8자 이상 12자 이하 +// 영문, 특수문자, 숫자 모두 최소 1개 이상 포함 + +// 한표 이름 제약조건 +// 2자 이상 +// 한글만 가능 + +// 한표 닉네임 제약조건 +// 1자 이상 +// 한글, 영문, 숫자 가능 + +// 한표 닉네임 제약조건 +// 1자 이상 +// 한글, 영문, 숫자 가능 + +const REGEX = { + EMAIL_ID: /^[a-z0-9_][a-z0-9_]{0,11}$/, + PASSWORD: /^(?=.*[a-zA-z])(?=.*[0-9])(?=.*[$`~!@$!%*#^?&\\(\\)\-_=+]).{8,12}$/, + NAME: /^[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]{2,}$/, + NICKNAME: /^[a-zA-Z0-9ㄱ-ㅎ|ㅏ-ㅣ|가-힣]{1,}$/, + GRADE: /^[1-4]{1}$/, + MAJOR: /^[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]{1,}$/, +}; + +function isEmailID(formValue: string): boolean { + return REGEX.EMAIL_ID.test(formValue); +} + +function isPassword(formValue: string): boolean { + return REGEX.PASSWORD.test(formValue); +} + +function isName(formValue: string): boolean { + return REGEX.NAME.test(formValue); +} + +function isNickname(formValue: string): boolean { + return REGEX.NICKNAME.test(formValue); +} + +function isGrade(formValue: string): boolean { + return REGEX.GRADE.test(formValue); +} + +function isMajor(formValue: string): boolean { + return REGEX.MAJOR.test(formValue); +} + +export { isEmailID, isPassword, isName, isNickname, isGrade, isMajor }; diff --git a/client/src/components/HOC/index.ts b/src/components/HOC/index.ts similarity index 100% rename from client/src/components/HOC/index.ts rename to src/components/HOC/index.ts diff --git a/client/src/components/HOC/withStoryBox/withStoryBox.tsx b/src/components/HOC/withStoryBox/withStoryBox.tsx similarity index 100% rename from client/src/components/HOC/withStoryBox/withStoryBox.tsx rename to src/components/HOC/withStoryBox/withStoryBox.tsx diff --git a/src/components/Skeleton/LectureListSkeleton/LectureListSkeleton.tsx b/src/components/Skeleton/LectureListSkeleton/LectureListSkeleton.tsx new file mode 100644 index 0000000..6e38946 --- /dev/null +++ b/src/components/Skeleton/LectureListSkeleton/LectureListSkeleton.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import loading from '@/assets/loading.gif'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '43rem', + height: '19rem', + marginTop: '1.2rem', + border: `1px solid ${theme.palette.grey[400]}`, + borderRadius: '0.7rem', + }, + img: { + width: '3rem', + height: '3rem', + marginBottom: '0.8rem', + }, + text: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '70%', + }, +})); + +const randomMessages = [ + '원하는 키워드로 강의를 찾아보세요! (ex: "기계")', + '강의 리뷰를 통해 다른 학생들에게 정보를 제공해보세요.', + '건의 사항이 있으시면 아래 Contact 메뉴를 통해 전달해주세요.', + '한표는 주기적으로 강의 정보를 업데이트 시키고 있습니다.', + '기분이 우울할 때는 제이레빗의 "요즘 너 말야"를 들어보세요.', + '오늘의 꿀팁 : 수강신청 한 번 망한다고 인생이 망하지는 않는답니다.', + '검색 팁 : 여러 과목을 한 번에 검색해보세요! (ex: 기계, 데이터)', + <> +

빨리 읽기 챌린지

+

저기있는 저 분은 박 법학박사이고, 여기있는 이 분은 백 법학박사이다.

+ , + <> +

빨리 읽기 챌린지

+

내가 그린 기린 그림은 긴 기린 그림이고 니가 그린 기린 그림은 안 긴 기린 그림이다!

+ , + <> +

빨리 읽기 챌린지

+

도토리가 문을 도로록, 드르륵, 두루룩 열었는가? 드로록, 도루륵, 두르룩 열었는가?

+ , +]; + +const LectureListSkeleton = () => { + const classes = useStyles(); + + const getRandomMessage = () => { + return randomMessages[Math.floor(Math.random() * randomMessages.length)]; + }; + + return ( +
+ loading animation + 강의 정보를 불러오고 있습니다. + + {getRandomMessage()} + +
+ ); +}; + +export { LectureListSkeleton }; diff --git a/src/components/Skeleton/index.ts b/src/components/Skeleton/index.ts new file mode 100644 index 0000000..681877a --- /dev/null +++ b/src/components/Skeleton/index.ts @@ -0,0 +1 @@ +export { LectureListSkeleton } from './LectureListSkeleton/LectureListSkeleton'; diff --git a/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx b/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx new file mode 100644 index 0000000..ec76846 --- /dev/null +++ b/src/components/UI/atoms/AlertSnackbar/AlertSnackbar.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import Snackbar from '@material-ui/core/Snackbar'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import { useReactiveVar } from '@apollo/client'; +import { useStores } from '@/stores'; + +enum SnackbarType { + ADD_SUCCESS = 'ADD_SUCCESS', + DELETE_SUCCESS = 'DELETE_SUCCESS', + SIGNUP_SUCCESS = 'SIGNUP_SUCCESS', + SIGNUP_FAILED = 'SIGNUP_FAILED', + LOGIN_SUCCESS = 'LOGIN_SUCCESS', + LOGIN_FAILED = 'LOGIN_FAILED', + NAV_FAILED = 'NAV_FAILED', + MY_SCHEDULE_ADD = 'MY_SCHEDULE_ADD', + DUPLICATE_LECTURE_NAME = 'DUPLICATE_LECTURE_NAME', + DUPLICATE_LECTURE_TIME = 'DUPLICATE_LECTURE_TIME', + INVALID_TIME = 'INVALID_TIME', + NO_TIMETABLE = 'NO_TIMETABLE', + SIGNUP_DUPLICATED_FAILED = 'SIGNUP_DUPLICATED_FAILED', +} + +const SNACKBAR_MESSAGE = { + [SnackbarType.ADD_SUCCESS]: '시간표가 추가되었습니다.', + [SnackbarType.DELETE_SUCCESS]: '시간표가 삭제되었습니다.', + [SnackbarType.SIGNUP_SUCCESS]: '정상적으로 회원가입되었습니다.', + [SnackbarType.SIGNUP_FAILED]: '회원가입이 실패하였습니다. 다시 시도해주세요.', + [SnackbarType.LOGIN_SUCCESS]: '정상적으로 로그인되었습니다.', + [SnackbarType.LOGIN_FAILED]: '로그인이 실패하였습니다. 다시 시도해주세요.', + [SnackbarType.NAV_FAILED]: '로그인이 필요한 서비스입니다. 로그인 후 시도해주세요.', + [SnackbarType.MY_SCHEDULE_ADD]: '나만의 시간표가 추가되었습니다.', + [SnackbarType.DUPLICATE_LECTURE_NAME]: '추가하려는 과목과 중복되는 과목이 있습니다.', + [SnackbarType.DUPLICATE_LECTURE_TIME]: '추가하려는 과목과 중복되는 시간이 있습니다.', + [SnackbarType.INVALID_TIME]: '시간이 유효하지 않습니다.', + [SnackbarType.NO_TIMETABLE]: '시간표를 만든 후 과목을 추가해주세요.', + [SnackbarType.SIGNUP_DUPLICATED_FAILED]: '중복체크가 실패하였습니다. 잠시 후 다시 시도해주세요.', +}; + +const AlertSnackbar = (): JSX.Element => { + const { snackbarStore } = useStores(); + const snackbarState = useReactiveVar(snackbarStore.state.snackbarState); + const snackbarType = useReactiveVar(snackbarStore.state.snackbarType); + + const onSnackbarCloseListener = (event: React.SyntheticEvent | React.MouseEvent, reason?: string) => { + if (reason === 'clickaway') { + return; + } + + snackbarStore.setSnackbarState(false); + }; + + return ( +
+ + + + } + /> +
+ ); +}; + +export { AlertSnackbar, SnackbarType }; diff --git a/src/components/UI/atoms/Button/Button.stories.tsx b/src/components/UI/atoms/Button/Button.stories.tsx new file mode 100644 index 0000000..38edf5e --- /dev/null +++ b/src/components/UI/atoms/Button/Button.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { Button, ButtonProps, ButtonType } from '@/components/UI/atoms'; +import { action } from '@storybook/addon-actions'; + +export default { + title: 'atom/Button', + component: Button, + decorators: [withKnobs], +} as Meta; + +const Template: Story = (args) => + ); +}; + +export { StyledButton, ButtonType }; +export type { ButtonProps }; diff --git a/client/src/components/UI/atoms/HashTag/HashTag.stories.tsx b/src/components/UI/atoms/HashTag/HashTag.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/HashTag/HashTag.stories.tsx rename to src/components/UI/atoms/HashTag/HashTag.stories.tsx diff --git a/client/src/components/UI/atoms/HashTag/HashTag.tsx b/src/components/UI/atoms/HashTag/HashTag.tsx similarity index 96% rename from client/src/components/UI/atoms/HashTag/HashTag.tsx rename to src/components/UI/atoms/HashTag/HashTag.tsx index cda9895..8130e38 100644 --- a/client/src/components/UI/atoms/HashTag/HashTag.tsx +++ b/src/components/UI/atoms/HashTag/HashTag.tsx @@ -15,6 +15,7 @@ const useStyles = makeStyles((theme) => ({ borderRadius: '1rem', padding: '0rem 0.5rem', marginRight: '0.5rem', + marginTop: '0.3rem', backgroundColor: 'white', }, })); diff --git a/client/src/components/UI/atoms/HeaderMenu/HeaderMenu.stories.tsx b/src/components/UI/atoms/HeaderMenu/HeaderMenu.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/HeaderMenu/HeaderMenu.stories.tsx rename to src/components/UI/atoms/HeaderMenu/HeaderMenu.stories.tsx diff --git a/client/src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx b/src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx similarity index 71% rename from client/src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx rename to src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx index 2bc35c2..9ddfbad 100644 --- a/client/src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx +++ b/src/components/UI/atoms/HeaderMenu/HeaderMenu.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import { Link } from 'react-router-dom'; interface HeaderMenuProps { children: React.ReactChild; link: string; + onHeaderMenuClick: (link: string) => void; } const useStyles = makeStyles((theme) => ({ @@ -16,6 +16,8 @@ const useStyles = makeStyles((theme) => ({ minHeight: '100%', boxSizing: 'border-box', margin: '1rem', + cursor: 'pointer', + '&:hover': { color: theme.palette.primary.main, borderBottom: `1rem solid ${theme.palette.primary.main}`, @@ -28,21 +30,20 @@ const useStyles = makeStyles((theme) => ({ text: { color: theme.palette.grey[500], }, - link: { - textDecoration: 'none', - }, })); -const HeaderMenu = ({ children, link }: HeaderMenuProps): JSX.Element => { +const HeaderMenu = ({ children, link, onHeaderMenuClick }: HeaderMenuProps): JSX.Element => { const classes = useStyles(); + const goLink = () => { + onHeaderMenuClick(link); + }; + return (
- - - {children} - - + + {children} +
); }; diff --git a/client/src/components/UI/atoms/LectureBox/LectureBox.tsx b/src/components/UI/atoms/LectureBox/LectureBox.tsx similarity index 64% rename from client/src/components/UI/atoms/LectureBox/LectureBox.tsx rename to src/components/UI/atoms/LectureBox/LectureBox.tsx index 2c23579..8d2dc8f 100644 --- a/client/src/components/UI/atoms/LectureBox/LectureBox.tsx +++ b/src/components/UI/atoms/LectureBox/LectureBox.tsx @@ -3,15 +3,14 @@ import { Typography, IconButton, Tooltip } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import DeleteIcon from '@material-ui/icons/Delete'; import { useStores } from '@/stores'; -import { SnackbarType } from '@/components/UI/atoms'; interface LectureBoxProps { startTime: number; endTime: number; bgcolor?: string; - name: string; - division?: string; - prof: string; + lectureName: string; + classNumber?: string | number; + professorName: string; } interface CSSProps { @@ -22,27 +21,31 @@ interface CSSProps { } const useStyles = makeStyles((theme) => ({ - root: { + root: ({ rowStartPos, rowEndPos, columnPos, bgcolor }: CSSProps) => ({ display: 'flex', flexDirection: 'column', position: 'absolute', - height: (props: CSSProps) => (props.rowStartPos * 2 + props.rowEndPos * 2 <= 40 ? `${props.rowEndPos * 2}rem` : '4rem'), + height: rowStartPos * 2 + rowEndPos * 2 <= 40 ? `${rowEndPos * 2}rem` : '4rem', width: '5rem', - left: (props: CSSProps) => `${5 + props.columnPos * 5}rem`, - top: (props: CSSProps) => (props.rowStartPos * 2 + props.rowEndPos * 2 <= 40 ? `${4 + props.rowStartPos * 2}rem` : '40rem'), + left: `${5 + columnPos * 5}rem`, + top: rowStartPos * 2 + rowEndPos * 2 <= 40 ? `${4 + rowStartPos * 2}rem` : '40rem', boxSizing: 'border-box', - backgroundColor: (props: CSSProps) => props.bgcolor || 'rgba(250, 244, 192)', + backgroundColor: bgcolor || 'rgba(250, 244, 192)', border: `1px solid ${theme.palette.grey[300]}`, borderTop: `2px solid ${theme.palette.grey[300]}`, + '&:hover': { boxShadow: '0 3px 4.5px 0 rgba(0, 0, 0, 0.16)', - '& > div[class*="makeStyles-membrane"]': { + + '&:first-child': { display: 'block', }, + '& .MuiButtonBase-root': { display: 'block', }, }, + '& .MuiButtonBase-root': { display: 'none', position: 'absolute', @@ -50,7 +53,7 @@ const useStyles = makeStyles((theme) => ({ top: '50%', left: '50%', }, - }, + }), membrane: { display: 'none', position: 'absolute', @@ -61,31 +64,32 @@ const useStyles = makeStyles((theme) => ({ }, })); -const LectureBox = ({ startTime, endTime, bgcolor, name, division, prof }: LectureBoxProps): JSX.Element => { +const LectureBox = ({ startTime, endTime, bgcolor, lectureName, classNumber, professorName }: LectureBoxProps): JSX.Element => { const columnPos = Math.floor(startTime / 1440); const rowStartPos = ((startTime % 1440) - 540) / 30; const rowEndPos = (endTime - startTime) / 30; const classes = useStyles({ columnPos, rowStartPos, rowEndPos, bgcolor }); + const { timeTableStore, snackbarStore } = useStores(); - const onClickHandler = () => { - timeTableStore.removeLectureFromTable(name); - snackbarStore.setSnackbarType(SnackbarType.DELETE_SUCCESS); - snackbarStore.setSnackbarState(true); + + const onLectureBoxClickListener = () => { + timeTableStore.removeLectureFromTable(lectureName); + snackbarStore.showTabDeleteMsg(); }; return (
- onClickHandler()}> +
- {name} + {lectureName}
- {`${division || '01'} ${prof}`} + {`${classNumber || '01'} ${professorName}`}
); diff --git a/client/src/components/UI/atoms/LectureGrid/LectureGrid.stories.tsx b/src/components/UI/atoms/LectureGrid/LectureGrid.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/LectureGrid/LectureGrid.stories.tsx rename to src/components/UI/atoms/LectureGrid/LectureGrid.stories.tsx diff --git a/client/src/components/UI/atoms/LectureGrid/LectureGrid.tsx b/src/components/UI/atoms/LectureGrid/LectureGrid.tsx similarity index 100% rename from client/src/components/UI/atoms/LectureGrid/LectureGrid.tsx rename to src/components/UI/atoms/LectureGrid/LectureGrid.tsx diff --git a/client/src/components/UI/atoms/LectureInfoDivider/LectureInfoDivider.tsx b/src/components/UI/atoms/LectureInfoDivider/LectureInfoDivider.tsx similarity index 100% rename from client/src/components/UI/atoms/LectureInfoDivider/LectureInfoDivider.tsx rename to src/components/UI/atoms/LectureInfoDivider/LectureInfoDivider.tsx diff --git a/client/src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx b/src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx similarity index 65% rename from client/src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx rename to src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx index 0ae0222..0d4f738 100644 --- a/client/src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx +++ b/src/components/UI/atoms/LectureInfoTitle/LectureInfoTitle.tsx @@ -11,15 +11,13 @@ enum LectureInfoTitleType { personnel = 'personnel', dept = 'dept', time = 'time', + room = 'room', + credit = 'credit', } interface TitleProps { className: LectureInfoTitleType; - children: any; - isHeader: boolean; -} - -interface CSSProps { + children?: JSX.Element[] | string | number; isHeader: boolean; } @@ -30,46 +28,59 @@ const useStyles = makeStyles((theme) => ({ alignItems: 'center', }, code: { - width: '10%', + width: '8%', }, name: { - width: '24%', + width: '19%', }, class: { - width: '5%', + width: '4%', }, prof: { - width: '12%', + width: '10%', }, grade: { - width: '5%', + width: '4%', }, personnel: { - width: '5%', + width: '4%', }, dept: { - width: '21%', + width: '19%', }, time: { - width: '18%', + width: '15%', + }, + room: { + width: '13%', + }, + credit: { + width: '4%', }, text: { display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', - color: (props: CSSProps) => (props.isHeader ? theme.palette.grey[500] : theme.palette.grey[800]), + color: theme.palette.grey[800], + }, + headerText: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + color: theme.palette.grey[500], }, })); const LectureInfoTitle = ({ className, children, isHeader }: TitleProps): JSX.Element => { - const classes = useStyles({ isHeader }); + const classes = useStyles(); const getClassName = () => { return { ...classes }[className]; }; return (
- + {children}
diff --git a/client/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.stories.tsx b/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.stories.tsx rename to src/components/UI/atoms/LectureReviewRating/LectureReviewRating.stories.tsx diff --git a/client/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx b/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx similarity index 50% rename from client/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx rename to src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx index 7b5a3e9..4a13a3e 100644 --- a/client/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx +++ b/src/components/UI/atoms/LectureReviewRating/LectureReviewRating.tsx @@ -3,29 +3,35 @@ import { makeStyles } from '@material-ui/core/styles'; import Star from '@material-ui/icons/Star'; import StarBorder from '@material-ui/icons/StarBorder'; import StarHalf from '@material-ui/icons/StarHalf'; +import { range } from '@/common/utils'; interface LectureReviewRatingProps { rating: number; } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles({ root: { display: 'flex', marginLeft: '0.5rem', }, -})); + star: { + cursor: 'pointer', + }, +}); const LectureReviewRating = ({ rating }: LectureReviewRatingProps): JSX.Element => { const classes = useStyles(); + const getStars = () => { - const array = []; - const Stars = Math.floor(rating); - const isHalfStar = !!(rating - Stars); - const borderStars = isHalfStar ? 4 - Stars : 5 - Stars; - for (let i = 0; i < Stars; i++) array.push(); - if (isHalfStar) array.push(); - for (let i = 0; i < borderStars; i++) array.push(); - return array; + const numOfStars = Math.floor(rating); + const hasHalfStar = !!(rating - numOfStars); + const stars = Array.from(range(1, 5)).map((num, idx) => { + if (idx < numOfStars) return ; + if (idx === numOfStars && hasHalfStar) return ; + return ; + }); + + return stars; }; return
{getStars()}
; diff --git a/src/components/UI/atoms/MyPageMenu/MyPageMenu.tsx b/src/components/UI/atoms/MyPageMenu/MyPageMenu.tsx new file mode 100644 index 0000000..9c3e3f9 --- /dev/null +++ b/src/components/UI/atoms/MyPageMenu/MyPageMenu.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Typography } from '@material-ui/core'; +import { useStores } from '@/stores'; + +enum MyPageMenuType { + MEMBER_INFO = 'MEMBER_INFO', + CHANGE_PASSWORD = 'CHANGE_PASSWORD', + MY_POSTING = 'MY_POSTING', + FRIEND_MANAGEMENT = 'FRIEND_MANAGEMENT', + SHARE = 'SHARE', + LOOKUP_CREDIT = 'LOOKUP_CREDIT', + WITHDRAWAL = 'WITHDRAWAL', +} + +interface MyPageMenuProps { + menuType: MyPageMenuType; +} + +const nameMapper = { + MEMBER_INFO: '회원정보', + CHANGE_PASSWORD: '비밀번호 변경', + MY_POSTING: '내 게시물', + FRIEND_MANAGEMENT: '친구 관리', + SHARE: '공유', + LOOKUP_CREDIT: '이수학점 현황 조회', + WITHDRAWAL: '회원탈퇴', +}; + +const useStyles = makeStyles((theme) => ({ + root: { + color: theme.palette.grey[500], + marginBottom: '0.7rem', + '&:hover': { + color: theme.palette.primary.main, + cursor: 'pointer', + }, + }, +})); + +const MyPageMenu = ({ menuType }: MyPageMenuProps) => { + const classes = useStyles(); + const { myPageStore } = useStores(); + + const onClickListener = () => { + myPageStore.state.menuType(MyPageMenuType[menuType]); + }; + + return ( + + {nameMapper[menuType]} + + ); +}; + +export { MyPageMenu, MyPageMenuType }; +export type { MyPageMenuProps }; diff --git a/client/src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx b/src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx similarity index 72% rename from client/src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx rename to src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx index 9eed775..d69fff2 100644 --- a/client/src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx +++ b/src/components/UI/atoms/SameLectureBox/SameLectureBox.tsx @@ -4,18 +4,18 @@ import { makeStyles } from '@material-ui/core/styles'; interface SameLectureBoxProps { startTime: number; endTime: number; - nowSelected?: boolean; + isSelectedLecture?: boolean; } interface CSSProps { columnPos: number; rowStartPos: number; rowEndPos: number; - nowSelected?: boolean; + isSelectedLecture?: boolean; } const useStyles = makeStyles((theme) => ({ - root: ({ columnPos, rowStartPos, rowEndPos, nowSelected }: CSSProps) => ({ + root: ({ columnPos, rowStartPos, rowEndPos, isSelectedLecture }: CSSProps) => ({ display: 'flex', flexDirection: 'column', position: 'absolute', @@ -24,15 +24,15 @@ const useStyles = makeStyles((theme) => ({ boxSizing: 'border-box', left: `${5 + columnPos * 5}rem`, top: rowStartPos * 2 + rowEndPos * 2 <= 40 ? `${4 + rowStartPos * 2}rem` : '40rem', - border: `${nowSelected ? 4 : 2}px solid ${theme.palette.primary.main}`, + border: `${isSelectedLecture ? 4 : 2}px solid ${theme.palette.primary.main}`, }), })); -const SameLectureBox = ({ startTime, endTime, nowSelected }: SameLectureBoxProps): JSX.Element => { +const SameLectureBox = ({ startTime, endTime, isSelectedLecture }: SameLectureBoxProps): JSX.Element => { const columnPos = Math.floor(startTime / 1440); const rowStartPos = ((startTime % 1440) - 540) / 30; const rowEndPos = (endTime - startTime) / 30; - const classes = useStyles({ columnPos, rowStartPos, rowEndPos, nowSelected }); + const classes = useStyles({ columnPos, rowStartPos, rowEndPos, isSelectedLecture }); return
; }; diff --git a/client/src/components/UI/atoms/SelectMenu/SelectMenu.stories.tsx b/src/components/UI/atoms/SelectMenu/SelectMenu.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/SelectMenu/SelectMenu.stories.tsx rename to src/components/UI/atoms/SelectMenu/SelectMenu.stories.tsx diff --git a/client/src/components/UI/atoms/SelectMenu/SelectMenu.tsx b/src/components/UI/atoms/SelectMenu/SelectMenu.tsx similarity index 83% rename from client/src/components/UI/atoms/SelectMenu/SelectMenu.tsx rename to src/components/UI/atoms/SelectMenu/SelectMenu.tsx index 405473a..e4f473a 100644 --- a/client/src/components/UI/atoms/SelectMenu/SelectMenu.tsx +++ b/src/components/UI/atoms/SelectMenu/SelectMenu.tsx @@ -11,10 +11,12 @@ interface MenuItemType { } interface SelectMenuProps { + value: string; + type: string; menuLabel: string; menus: MenuItemType[]; dropMenuWidth?: number | string; - onSelectMenuChange: () => void; + onMenuClick: (e: React.MouseEvent) => void; } interface cssProps { @@ -71,16 +73,14 @@ const useStyles = makeStyles((theme: Theme) => }), ); -const SelectMenu = ({ menuLabel, menus, dropMenuWidth = 'auto', onSelectMenuChange }: SelectMenuProps): JSX.Element => { +const SelectMenu = ({ value, type, menuLabel, menus, dropMenuWidth = 'auto', onMenuClick }: SelectMenuProps): JSX.Element => { const [anchorEl, setAnchorEl] = useState(null); - const [selectValue, setSelectValue] = useState(''); - const open = Boolean(anchorEl); const classes = useStyles({ open, dropMenuWidth }); const getMenuItems = (): JSX.Element[] => { const menuItems = menus.map((menu) => ( -
  • +
  • {menu.title}
  • )); @@ -107,26 +107,16 @@ const SelectMenu = ({ menuLabel, menus, dropMenuWidth = 'auto', onSelectMenuChan setAnchorEl(null); }; - const onMenuClickListener = (event: React.MouseEvent) => { - const target = event.target as HTMLElement; - const liElement = target.closest('li'); - - if (!liElement) return; - - const { dataset } = liElement; - setSelectValue(dataset?.title ?? ''); + const onMenuClickListener = (e: React.MouseEvent) => { + onMenuClick(e); setAnchorEl(null); - - if (onSelectMenuChange) { - onSelectMenuChange(); - } }; return ( <>
    - {selectValue || menuLabel} - + {value || menuLabel} +
    void; + onClick?: (event: React.MouseEvent) => void; + detail: boolean; } interface CSSProps { @@ -28,20 +29,27 @@ const useStyles = makeStyles((theme) => ({ }), down: { color: '#F15F5F', - marginRight: '0.2rem', }, up: { color: '#6799FF', + }, + marginRight: { marginRight: '0.2rem', }, })); -const Thumb = ({ thumbDown = false, score, onClick }: ThumbProps): JSX.Element => { +const Thumb = ({ thumbDown = false, score, onClick, detail }: ThumbProps): JSX.Element => { const classes = useStyles({ thumbDown }); return (
    - {thumbDown ? : } - {score} + {thumbDown ? ( + + ) : ( + + )} + + {score} +
    ); }; diff --git a/client/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.stories.tsx b/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.stories.tsx similarity index 100% rename from client/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.stories.tsx rename to src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.stories.tsx diff --git a/client/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx b/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx similarity index 74% rename from client/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx rename to src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx index 3ace47d..f47a43e 100644 --- a/client/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx +++ b/src/components/UI/atoms/TimeSelectMenu/TimeSelectMenu.tsx @@ -19,9 +19,13 @@ interface TimeSelectMenuDataType { } interface TimeSelectMenuProps { + selected: boolean; + value: string; + selectType: string; menuLabel: string; dropMenuWidth?: number | string; - onSelectMenuChange: () => void; + onMenuClick: (e: React.MouseEvent) => void; + checkSelectedItem: (itemValue: string) => boolean; } interface cssProps { @@ -112,21 +116,19 @@ const MINUTE_DATAS = Array.from(range(0, 30, 30)).map((minute, idx) => ({ type: TimeSelectMenuItemType.MINUTE, })); -const TimeSelectMenu = ({ menuLabel, dropMenuWidth = 'auto', onSelectMenuChange }: TimeSelectMenuProps): JSX.Element => { - const [anchorEl, setAnchorEl] = React.useState(null); - const [isSelected, setIsSelected] = useState(false); - const [selectedAMPM, setSelectedAMPM] = useState('오전'); - const [selectedHour, setSelectedHour] = useState('01'); - const [selectedMinute, setSelectedMinute] = useState('00'); - - const selectedValue = `${selectedAMPM}${selectedHour && ` ${selectedHour} : `}${selectedMinute}`; - +const TimeSelectMenu = ({ + selected, + selectType, + value, + menuLabel, + dropMenuWidth = 'auto', + checkSelectedItem, + onMenuClick, +}: TimeSelectMenuProps): JSX.Element => { + const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const classes = useStyles({ open, dropMenuWidth }); - const isSelectedMenuItem = (value: string) => { - return value === selectedAMPM || value === selectedHour || value === selectedMinute; - }; + const classes = useStyles({ open, dropMenuWidth }); const getMenuItems = (menuItemDatas: TimeSelectMenuDataType[]): JSX.Element[] => { const menuItems = menuItemDatas.map((menuItemData) => ( @@ -135,8 +137,9 @@ const TimeSelectMenu = ({ menuLabel, dropMenuWidth = 'auto', onSelectMenuChange data-value={menuItemData.value} data-title={menuItemData.title} data-type={menuItemData.type} - data-selected={isSelectedMenuItem(menuItemData.title)} - onClick={onMenuClickListener}> + data-selected={checkSelectedItem(menuItemData.title)} + data-selectType={selectType} + onClick={onMenuClick}> {menuItemData.title} )); @@ -158,38 +161,17 @@ const TimeSelectMenu = ({ menuLabel, dropMenuWidth = 'auto', onSelectMenuChange scrollDown(eventTarget); setAnchorEl(eventTarget); - setIsSelected(true); }; const onMenuCloseListener = () => { setAnchorEl(null); }; - const onMenuClickListener = (event: React.MouseEvent) => { - const target = event.target as HTMLElement; - const liElement = target.closest('li'); - - if (!liElement) return; - - const { dataset } = liElement; - const { type, title } = dataset; - - if (type === TimeSelectMenuItemType.AM_PM) setSelectedAMPM(title ?? ''); - if (type === TimeSelectMenuItemType.HOUR) { - setSelectedHour(title ?? ''); - } - if (type === TimeSelectMenuItemType.MINUTE) setSelectedMinute(title ?? ''); - - if (onSelectMenuChange) { - onSelectMenuChange(); - } - }; - return ( <>
    - {isSelected ? selectedValue : menuLabel} - + {selected ? value : menuLabel} +
    = (args) => { + const FindModalContentStory = withStoryBox(args, 700)(FindModalContent); + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + modalType: FindModalType.FIND_MODAL, + valid: [true, true], + isFindDisabled: true, + onFindBtnClick: action('onClick'), + onInputChange: action('onChange'), +}; diff --git a/src/components/UI/molecules/FindModalContent/FindModalContent.tsx b/src/components/UI/molecules/FindModalContent/FindModalContent.tsx new file mode 100644 index 0000000..11f8700 --- /dev/null +++ b/src/components/UI/molecules/FindModalContent/FindModalContent.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { DialogTitle, DialogContent, DialogActions, TextField, Typography } from '@material-ui/core'; +import { Button, ButtonType } from '@/components/UI/atoms'; +import { makeStyles } from '@material-ui/core/styles'; + +enum FindModalType { + FIND_MODAL = 'FIND_MODAL', +} + +interface FindModalContentProps { + valid: boolean[]; + isFindDisabled: boolean; + onFindBtnClick: () => void; + onInputChange: () => void; +} + +const useStyles = makeStyles((theme) => ({ + title: { + display: 'flex', + justifyContent: 'center', + fontSize: '1.7rem', + color: theme.palette.primary.main, + }, + dialogContentRoot: { + paddingTop: 0, + + '&.MuiDialogContent-root': { + overflow: 'hidden', + }, + }, + dialogActionRoot: { + display: 'flex', + flexDirection: 'column', + + '&.MuiDialogActions-root': { + padding: '1rem 1.5rem', + }, + }, + msgArea: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginBottom: '20px', + + '& > p': { + marginBottom: '3px', + fontSize: '13px', + + '&:last-of-type': { + marginBottom: 0, + }, + }, + }, + linkTextArea: { + display: 'flex', + justifyContent: 'flex-end', + width: '100%', + paddingTop: '1rem', + margin: '0 !important', + }, + linkText: { + color: theme.palette.grey[600], + }, +})); + +const HELPER_TEXT = { + EMAIL: { + DEFAULT: '아이디는 한기대 포털 아이디입니다. @koreatech.ac.kr은 빼고 입력해주세요.', + ERROR: '한기대 포털 아이디는 1자리 이상 12자리 이하 / 영소문자, 숫자, _ 로만 조합되어야합니다. ', + }, + NAME: { + DEFAULT: '이름을 입력해주세요.', + ERROR: '이름은 최소 2자 이상 / 한글로만 조합되어야합니다.', + }, +}; + +const LOGIN_BUTTON_STYLE_PROPS = { width: 192, height: 35.2, borderRadius: 4, fontSize: 16 }; + +const FindModalContent = ({ valid, isFindDisabled, onInputChange, onFindBtnClick }: FindModalContentProps): JSX.Element => { + const classes = useStyles(); + const [isValidEmail, isValidName] = valid; + + return ( + <> + + 비밀번호 찾기 + + +
    +

    비밀번호는 이름, 가입한 아이디를 통해 랜덤하게 초기화됩니다.

    +

    초기화된 비밀번호로 로그인 후 반드시 변경해주세요.

    +
    + + +
    + + +
    + + 입력한 내용이 올바르다면 초기화된 비밀번호가 이메일로 발송됩니다. + +
    +
    + + ); +}; + +export { FindModalContent, FindModalType }; +export type { FindModalContentProps }; diff --git a/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx b/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx new file mode 100644 index 0000000..e5491ac --- /dev/null +++ b/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { MY_MEMBER_INFO } from '@/queries'; +import { useQuery } from '@apollo/client'; +import { GetMyMemberInfo } from '@/api'; +import { UserProfile, HeaderLoginMenu } from '@/components/UI/molecules'; + +const HeaderAuthSection = (): JSX.Element => { + const { loading, error, data } = useQuery(MY_MEMBER_INFO); + + if (loading || error) return ; + + const { myMemberInfo } = data as GetMyMemberInfo; + + return ; +}; + +export { HeaderAuthSection }; diff --git a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.stories.tsx b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.stories.tsx similarity index 53% rename from client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.stories.tsx rename to src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.stories.tsx index cda5bae..53901b4 100644 --- a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSection.stories.tsx +++ b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.stories.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { withKnobs } from '@storybook/addon-knobs'; import { Story, Meta } from '@storybook/react/types-6-0'; -import { HeaderAuthSection } from '@/components/UI/molecules'; +import { HeaderLoginMenu } from '@/components/UI/molecules'; export default { - title: 'molecules/HeaderAuthSection', - component: HeaderAuthSection, + title: 'molecules/HeaderLoginMenu', + component: HeaderLoginMenu, decorators: [withKnobs], } as Meta; -const Template: Story = (args) => ; +const Template: Story = (args) => ; export const Default = Template.bind({}); diff --git a/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.tsx b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.tsx new file mode 100644 index 0000000..cfb2d16 --- /dev/null +++ b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenu.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useStores } from '@/stores'; +import { HeaderLoginMenuArea } from './HeaderLoginMenuArea'; + +const HeaderLoginMenu = (): JSX.Element => { + const { modalStore } = useStores(); + + const onLoginBtnClickListener = () => { + modalStore.openLoginModal(); + }; + + const onSignUpBtnClickListener = () => { + modalStore.openSignUpModal(); + }; + + const onFindPasswordBtnClickListener = () => { + modalStore.openFindModal(); + }; + + return ( + + ); +}; + +export { HeaderLoginMenu }; diff --git a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSectionArea.tsx b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenuArea.tsx similarity index 64% rename from client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSectionArea.tsx rename to src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenuArea.tsx index 6839507..6d5734b 100644 --- a/client/src/components/UI/molecules/HeaderAuthSection/HeaderAuthSectionArea.tsx +++ b/src/components/UI/molecules/HeaderLoginMenu/HeaderLoginMenuArea.tsx @@ -3,11 +3,14 @@ import { Typography } from '@material-ui/core'; import { Button, ButtonType } from '@/components/UI/atoms'; import { makeStyles } from '@material-ui/core/styles'; -interface HeaderAuthSectionAreaProps { - onLoginClick: () => void; - onSignUpClick: () => void; +interface HeaderLoginMenuAreaProps { + onLoginBtnClick: () => void; + onSignUpBtnClick: () => void; + onFindPasswordBtnClick: () => void; } +const BUTTON_STYLE_PROPS = { width: 192, height: 35.2, borderRadius: 4, fontSize: 19.2 }; + const useStyles = makeStyles((theme) => ({ loginSection: { display: 'flex', @@ -22,11 +25,13 @@ const useStyles = makeStyles((theme) => ({ }, divider: { borderLeft: `1px solid ${theme.palette.grey[500]}`, - margin: '0 3px', + margin: '0 0.3125rem', }, authText: { color: theme.palette.grey[600], '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline', cursor: 'pointer', }, }, @@ -35,27 +40,28 @@ const useStyles = makeStyles((theme) => ({ }, })); -const HeaderAuthSectionArea = ({ onLoginClick, onSignUpClick }: HeaderAuthSectionAreaProps): JSX.Element => { +const HeaderLoginMenuArea = ({ onLoginBtnClick, onSignUpBtnClick, onFindPasswordBtnClick }: HeaderLoginMenuAreaProps): JSX.Element => { const classes = useStyles(); + return (
    한표를 더 편리하게 이용하세요 -
    - + 회 원 가 입
    - - 아 이 디 / 비 밀 번 호 찾기 + + 비 밀 번 호 찾 기
    ); }; -export { HeaderAuthSectionArea }; +export { HeaderLoginMenuArea }; diff --git a/src/components/UI/molecules/HeaderNavSection/HeaderNavSection.tsx b/src/components/UI/molecules/HeaderNavSection/HeaderNavSection.tsx new file mode 100644 index 0000000..e57fc8b --- /dev/null +++ b/src/components/UI/molecules/HeaderNavSection/HeaderNavSection.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { HeaderMenu } from '@/components/UI/atoms'; +import { useHistory } from 'react-router-dom'; +import { useApolloClient } from '@apollo/client'; +import { MY_MEMBER_INFO } from '@/queries'; +import { useStores } from '@/stores'; + +interface NavMenu { + link: string; + name: string; +} + +const MAIN_PAGE_LINK = '/'; + +const NAV_MENUS = [ + { link: '/', name: '시간표짜기' }, + { link: '/review', name: '강의후기' }, +]; + +const HeaderNavSection = (): JSX.Element => { + const history = useHistory(); + const client = useApolloClient(); + const { modalStore, snackbarStore } = useStores(); + + const checkAuthState = () => { + const meberInfo = client.readQuery({ query: MY_MEMBER_INFO }); + + return !!meberInfo; + }; + + const onHeaderMenuClickListener = (link: string) => { + if (link !== MAIN_PAGE_LINK && !checkAuthState()) { + snackbarStore.showNavFailedMsg(); + history.push(MAIN_PAGE_LINK); + modalStore.openLoginModal(); + + return; + } + history.push(link); + }; + + const gerHeaderMenus = (): JSX.Element[] => { + return NAV_MENUS.map((navMenu: NavMenu) => ( + + {navMenu.name} + + )); + }; + + return <>{gerHeaderMenus()}; +}; + +export { HeaderNavSection }; diff --git a/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx b/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx new file mode 100644 index 0000000..4895ae5 --- /dev/null +++ b/src/components/UI/molecules/LectureBoxContainer/LectureBoxContainer.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { LectureBox, SameLectureBox } from '@/components/UI/atoms'; +import { TimeTypes } from '@/components/UI/molecules'; +import { useStores } from '@/stores'; +import { useReactiveVar } from '@apollo/client'; +import { isString } from '@/common/utils/typeCheck'; + +const useStyles = makeStyles({ + root: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + }, +}); + +const LectureBoxContainer = (): JSX.Element => { + const classes = useStyles(); + const { timeTableStore, lectureInfoStore } = useStores(); + const savedLectures = useReactiveVar(timeTableStore.state.selectedTabLectures); + const selectedTabIdx = useReactiveVar(timeTableStore.state.selectedTabIdx); + const nowSelectedLecture = useReactiveVar(lectureInfoStore.state.selectedLecture); + + const getLectureBoxes = () => { + if (selectedTabIdx === 0) return <>; + + const lectureInfos = savedLectures[selectedTabIdx - 1]; + if (!lectureInfos) return <>; + + return lectureInfos.map((lectureInfo) => { + if (isString(lectureInfo.lectureTimes)) return <>; + + const times = lectureInfo.lectureTimes as TimeTypes[]; + + if (!times) return []; + + return times.map((time) => { + return ( + + ); + }); + }); + }; + + const getSameLectureBoxes = () => { + const sameLectures = lectureInfoStore.getSameLectures(); + + return sameLectures.map((sameLecture) => { + if (!sameLecture) return sameLecture; + + if (typeof sameLecture.lectureTimes === 'string') return <>; + + if (!sameLecture.lectureTimes) return []; + + return sameLecture.lectureTimes.map((time) => { + if (!time) return time; + + if (sameLecture.divisionNumber === nowSelectedLecture?.divisionNumber) { + return ; + } + return ; + }); + }); + }; + + return ( +
    + {getLectureBoxes()} + {getSameLectureBoxes()} +
    + ); +}; + +export { LectureBoxContainer }; diff --git a/client/src/components/UI/molecules/LectureInfo/LectureInfo.stories.tsx b/src/components/UI/molecules/LectureInfo/LectureInfo.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureInfo/LectureInfo.stories.tsx rename to src/components/UI/molecules/LectureInfo/LectureInfo.stories.tsx diff --git a/client/src/components/UI/molecules/LectureInfo/LectureInfo.tsx b/src/components/UI/molecules/LectureInfo/LectureInfo.tsx similarity index 53% rename from client/src/components/UI/molecules/LectureInfo/LectureInfo.tsx rename to src/components/UI/molecules/LectureInfo/LectureInfo.tsx index ebfc80c..f9ef89b 100644 --- a/client/src/components/UI/molecules/LectureInfo/LectureInfo.tsx +++ b/src/components/UI/molecules/LectureInfo/LectureInfo.tsx @@ -4,6 +4,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { LectureInfoTitle, LectureInfoTitleType, LectureInfoDivider } from '@/components/UI/atoms'; import { useStores } from '@/stores'; import { useReactiveVar } from '@apollo/client'; +import { isString } from '@/common/utils/typeCheck'; const useStyles = makeStyles((theme) => ({ root: { @@ -47,14 +48,18 @@ interface TimeTypes { } interface LectureInfos { + id?: number; code: string; name: string; - class: string; - prof: string; - grade: string; - personnel: string; - dept: string; - time: Array | string; + divisionNumber: number | string; + professor: string; + totalStudentNumber: number | string; + department: string; + lectureTimes: Array | string; + room?: string; + requiredGrade?: number | string; + requiredMajor?: string; + credit?: number | string; color?: string; } @@ -66,74 +71,101 @@ interface LectureInfoProps { isBasketList?: boolean; } +const LECTURE_INFO_TOOLTIP_MESSAGE = { + REMOVE_MESSAGE: '시간표에서 제거하기', + ADD_MESSAGE: '시간표에 추가하기', +}; + const LectureInfo = ({ isHeader = false, infos, onDoubleClick, onClick, isBasketList = false }: LectureInfoProps): JSX.Element => { const classes = useStyles(); const subClass = isHeader ? classes.header : classes.item; + const { lectureInfoStore } = useStores(); + const nowSelectedLectureInfo = isBasketList ? useReactiveVar(lectureInfoStore.state.basketSelectedLecture) : useReactiveVar(lectureInfoStore.state.selectedLecture); - const isSelectedLecture = () => { - return nowSelectedLectureInfo?.code === infos.code && nowSelectedLectureInfo?.class === infos.class; + + const checkSelectedLecture = (): boolean => { + return nowSelectedLectureInfo?.code === infos.code && nowSelectedLectureInfo?.divisionNumber === infos.divisionNumber; }; - const convertNumberToTime = (time: number) => { + + const convertNumberToTime = (time: number): string => { const hour = Math.floor((time % 1440) / 60) .toString() .padStart(2, '0'); const minute = (time % 60).toString().padEnd(2, '0'); + return `${hour}:${minute}`; }; - const convertTimeToString = (times: TimeTypes) => { + + const convertTimeToString = (times: TimeTypes): string => { const days = ['월', '화', '수', '목', '금', '토']; const startDay = days[Math.floor(times.start / 1440)]; const startTime = convertNumberToTime(times.start); const endTime = convertNumberToTime(times.end); + return `${startDay} ${startTime} - ${endTime}`; }; - const getLectureTime = (times: Array | string) => { - if (typeof times === 'string') return times; - return times.map((time) => { - return {convertTimeToString(time)}; + + const getLectureTimes = (times: Array | string): JSX.Element[] | string => { + if (isString(times)) return times as string; + if (!times) return [<>]; + + return (times as TimeTypes[]).map((time) => { + return ( + + {convertTimeToString(time)} + + ); }); }; + + const lectureInfoArray = [ + { type: LectureInfoTitleType.code, children: infos.code }, + { type: LectureInfoTitleType.name, children: infos.name }, + { type: LectureInfoTitleType.class, children: infos.divisionNumber }, + { type: LectureInfoTitleType.prof, children: infos.professor }, + { type: LectureInfoTitleType.personnel, children: infos.totalStudentNumber }, + { type: LectureInfoTitleType.grade, children: infos.requiredMajor }, + { type: LectureInfoTitleType.dept, children: infos.department }, + { type: LectureInfoTitleType.room, children: infos.room }, + { type: LectureInfoTitleType.credit, children: infos.credit }, + ]; + + const getLectureInfo = () => { + const lectureInfo = lectureInfoArray.map((info) => { + return ( + <> + + {info.children} + + + + ); + }); + const timeInfo = ( + <> + + {getLectureTimes(infos.lectureTimes)} + + + ); + return [...lectureInfo, timeInfo]; + }; + return ( - +
    onClick(infos)} onDoubleClick={() => onDoubleClick(infos)}> - - {infos.code} - - - - {infos.name} - - - - {infos.class} - - - - {infos.prof} - - - - {infos.grade} - - - - {infos.personnel} - - - - {infos.dept} - - - - {getLectureTime(infos.time)} - + {getLectureInfo()}
    ); diff --git a/src/components/UI/molecules/LectureListContent/LectureListContent.tsx b/src/components/UI/molecules/LectureListContent/LectureListContent.tsx new file mode 100644 index 0000000..8b1e22c --- /dev/null +++ b/src/components/UI/molecules/LectureListContent/LectureListContent.tsx @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { LectureInfo, LectureInfos } from '@/components/UI/molecules'; + +interface LectureListContentProps { + isBasket?: boolean; + lectureInfos: LectureInfos[] | null; + onDoubleClick: (lectureInfos: LectureInfos) => void; + onClick: (lectureInfos: LectureInfos) => void; +} + +interface CSSProps { + isBasket?: boolean; +} + +const useStyles = makeStyles((theme) => ({ + rootWrapper: { + width: '43rem', + height: (props: CSSProps) => (props.isBasket ? '12rem' : '19rem'), + margin: '1.2rem 0 0 0', + padding: '0 0.2rem 0.4rem 0.2rem', + boxSizing: 'border-box', + border: `1px solid ${theme.palette.grey[400]}`, + borderRadius: '0.7rem', + }, + root: { + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + boxSizing: 'border-box', + alignItems: 'center', + }, + body: { + display: 'flex', + flexDirection: 'column', + marginLeft: '0.25rem', + width: '100%', + alignItems: 'center', + overflow: 'auto', + '&::-webkit-scrollbar': { + width: '0.3rem', + display: 'block', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: theme.palette.grey[300], + borderRadius: '0.7rem', + }, + }, + empty: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: (props: CSSProps) => (props.isBasket ? '10rem' : '17rem'), + }, +})); + +const headerInfos = { + code: '코드', + name: '강의명', + divisionNumber: '분반', + professor: '교수님', + requiredMajor: '대상', + totalStudentNumber: '정원', + department: '개설학부', + room: '강의실', + credit: '학점', + lectureTimes: '시간', +}; + +const LectureListContent = ({ isBasket = false, lectureInfos, onDoubleClick, onClick }: LectureListContentProps): JSX.Element => { + const classes = useStyles({ isBasket }); + + const getLectureInfos = (lectureInfoDatas: Array | null): JSX.Element[] => { + if (!lectureInfoDatas) { + if (isBasket) return [
    추가된 과목이 없습니다.
    ]; + return [
    검색을 통해 강의를 찾아보세요!
    ]; + } + if (lectureInfoDatas.length === 0) { + if (isBasket) return [
    추가된 과목이 없습니다.
    ]; + return [
    설정된 조건에 맞는 데이터가 없습니다.
    ]; + } + + return lectureInfoDatas.map((lectureInfoData: LectureInfos) => { + return ( + + ); + }); + }; + + return ( +
    +
    + {}} onClick={() => {}} /> +
    {getLectureInfos(lectureInfos)}
    +
    +
    + ); +}; + +export { LectureListContent }; +export type { LectureListContentProps }; diff --git a/client/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.stories.tsx b/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.stories.tsx rename to src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.stories.tsx diff --git a/client/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx b/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx similarity index 58% rename from client/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx rename to src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx index 5f2b2ec..a46fcd0 100644 --- a/client/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx +++ b/src/components/UI/molecules/LectureReviewHashTags/LectureReviewHashTags.tsx @@ -1,22 +1,26 @@ +/* eslint-disable react/no-array-index-key */ import React from 'react'; import { HashTag } from '@/components/UI/atoms'; import { makeStyles } from '@material-ui/core/styles'; interface LectureReviewHashTagsProps { tags: string[]; + isDetail?: boolean; } const useStyles = makeStyles((theme) => ({ root: { display: 'flex', + flexWrap: 'wrap', }, })); -const LectureReviewHashTags = ({ tags }: LectureReviewHashTagsProps): JSX.Element => { +const LectureReviewHashTags = ({ tags, isDetail = false }: LectureReviewHashTagsProps): JSX.Element => { const classes = useStyles(); const getHashTags = () => { - return tags.map((tag) => { - return {tag}; + const tempTags = isDetail ? tags : tags.slice(0, 9); + return tempTags.map((tag, idx) => { + return {tag}; }); }; return
    {getHashTags()}
    ; diff --git a/client/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.stories.tsx b/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.stories.tsx rename to src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.stories.tsx diff --git a/client/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.tsx b/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.tsx rename to src/components/UI/molecules/LectureReviewInfo/LectureReviewInfo.tsx diff --git a/client/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.stories.tsx b/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.stories.tsx rename to src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.stories.tsx diff --git a/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx b/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx new file mode 100644 index 0000000..2d23855 --- /dev/null +++ b/src/components/UI/molecules/LectureReviewThumbs/LectureReviewThumbs.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Thumb } from '@/components/UI/atoms'; + +interface LectureReviewThumbsProps { + upScore: number; + downScore: number; + detail?: boolean; + isMine?: boolean; +} + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + }, +})); + +const LectureReviewThumbs = ({ upScore, downScore, detail = false, isMine }: LectureReviewThumbsProps): JSX.Element => { + const classes = useStyles(); + + const onUpClickListener = (event: React.MouseEvent) => { + event.stopPropagation(); + if (isMine) return; + console.log('up click'); + }; + + const onDownClickListener = (event: React.MouseEvent) => { + event.stopPropagation(); + if (isMine) return; + console.log('down click'); + }; + + return ( +
    + + +
    + ); +}; + +export { LectureReviewThumbs }; +export type { LectureReviewThumbsProps }; diff --git a/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitle.tsx b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitle.tsx new file mode 100644 index 0000000..d882ec6 --- /dev/null +++ b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitle.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { LectureReviewTitleContent } from './LectureReviewTitleContent'; + +const LectureReviewTitle = (): JSX.Element => { + const history = useHistory(); + + const onWriteBtnClickListener = () => { + history.push('/reviewWrite'); + }; + + return ; +}; + +export { LectureReviewTitle }; diff --git a/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.stories.tsx b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.stories.tsx new file mode 100644 index 0000000..0ad763f --- /dev/null +++ b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { action } from '@storybook/addon-actions'; +import { withStoryBox } from '@/components/HOC'; +import { LectureReviewTitleContent, LectureReviewTitleContentProps } from './LectureReviewTitleContent'; + +export default { + title: 'molecules/LectureReviewTitle', + component: LectureReviewTitleContent, + decorators: [withKnobs], +} as Meta; + +const Template: Story = (args) => { + const LectureReviewTitleStory = withStoryBox(args, 640)(LectureReviewTitleContent); + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + onWriteBtnClick: action('onClick'), +}; diff --git a/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.tsx b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.tsx new file mode 100644 index 0000000..631e16a --- /dev/null +++ b/src/components/UI/molecules/LectureReviewTitle/LectureReviewTitleContent.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Typography } from '@material-ui/core'; +import { Button, ButtonType } from '@/components/UI/atoms'; + +interface LectureReviewTitleContentProps { + onWriteBtnClick: () => void; +} + +const BUTTON_STYLE_PROPS = { width: 160, height: 36.3, borderRadius: 16, fontSize: 20.8 }; + +const useStyles = makeStyles({ + titleArea: { + width: '100%', + marginTop: '1.5rem', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, +}); + +const LectureReviewTitleContent = ({ onWriteBtnClick }: LectureReviewTitleContentProps): JSX.Element => { + const classes = useStyles(); + + return ( +
    + + 강의 후기 + + +
    + ); +}; + +export { LectureReviewTitleContent }; +export type { LectureReviewTitleContentProps }; diff --git a/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.stories.tsx b/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.stories.tsx rename to src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.stories.tsx diff --git a/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx b/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx similarity index 63% rename from client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx rename to src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx index 293e50d..dbef518 100644 --- a/client/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx +++ b/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilter.tsx @@ -1,21 +1,25 @@ import React from 'react'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import { SelectMenu, TimeSelectMenu, SelectMenuProps, TimeSelectMenuProps } from '@/components/UI/atoms'; +import { SelectMenu, TimeSelectMenu, SelectMenuProps, TimeSelectMenuProps, Button, ButtonType } from '@/components/UI/atoms'; interface LectureSearchFilterProps { majorSelectMenu: SelectMenuProps; daySelectMenu: SelectMenuProps; gradeSelectMenu: SelectMenuProps; - timeSelectMenu: TimeSelectMenuProps; + startTimeSelectMenu: TimeSelectMenuProps; + endTimeSelectMenu: TimeSelectMenuProps; + onInitButtonClickListener: () => void; } const DROP_MENU_WIDTH = { - MAJOR: '10rem', - DAY: '4.1875rem', - GRADE: '4.5rem', - TIME: '6.75rem', + MAJOR: '10.5rem', + DAY: '4.6875rem', + GRADE: '5rem', + TIME: '7.25rem', }; +const BUTTON_STYLE_PROPS = { width: 80, height: 32.25, borderRadius: 11.2, fontSize: 12 }; + const useStyles = makeStyles((theme: Theme) => createStyles({ root: { @@ -23,7 +27,7 @@ const useStyles = makeStyles((theme: Theme) => justifyContent: 'space-between', alignItems: 'center', marginTop: '1.2rem', - + width: '100%', '& > *': { marginRight: '0.3125rem', }, @@ -47,7 +51,14 @@ const useStyles = makeStyles((theme: Theme) => }), ); -const LectureSearchFilter = ({ majorSelectMenu, daySelectMenu, gradeSelectMenu, timeSelectMenu }: LectureSearchFilterProps): JSX.Element => { +const LectureSearchFilter = ({ + majorSelectMenu, + daySelectMenu, + gradeSelectMenu, + startTimeSelectMenu, + endTimeSelectMenu, + onInitButtonClickListener, +}: LectureSearchFilterProps): JSX.Element => { const classes = useStyles(); return ( @@ -62,11 +73,16 @@ const LectureSearchFilter = ({ majorSelectMenu, daySelectMenu, gradeSelectMenu,
    - +
    ~
    - + +
    +
    +
    ); diff --git a/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx b/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx new file mode 100644 index 0000000..55fab31 --- /dev/null +++ b/src/components/UI/molecules/LectureSearchFilter/LectureSearchFilterMenu.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { useStores, LectureFilterType } from '@/stores'; +import { useTimeSelectMenu, useSelectMenu } from '@/common/hooks'; +import { LectureSearchFilter } from './LectureSearchFilter'; + +interface SelectMenuState { + department: string; + day: string; + credit: string; +} + +const DEPARTMENT_MENU = [ + { id: 0, title: '컴퓨터공학부', value: 0 }, + { id: 1, title: '디자인ㆍ건축공학부', value: 1 }, + { id: 2, title: '기계공학부', value: 2 }, + { id: 3, title: '메카트로닉스공학부', value: 3 }, + { id: 4, title: '전기ㆍ전자ㆍ통신공학부', value: 4 }, + { id: 5, title: '에너지신소재화학공학부', value: 5 }, + { id: 6, title: '산업경영학부', value: 6 }, + { id: 7, title: '교양학부', value: 7 }, + { id: 8, title: 'HRD학과', value: 8 }, + { id: 9, title: '전체', value: 9 }, +]; + +const SELECT_MENU = [ + { id: 0, title: '월', value: 0 }, + { id: 1, title: '화', value: 1 }, + { id: 2, title: '수', value: 2 }, + { id: 3, title: '목', value: 3 }, + { id: 4, title: '금', value: 4 }, + { id: 5, title: '토', value: 5 }, + { id: 6, title: '전체', value: 6 }, +]; + +const CREDIT_MENU = [ + { id: 0, title: '1학점', value: 0 }, + { id: 1, title: '2학점', value: 1 }, + { id: 2, title: '3학점', value: 3 }, + { id: 3, title: '4학점', value: 4 }, + { id: 4, title: '전체', value: 5 }, +]; + +const INIT_SELECT_STATE = { department: '', day: '', credit: '' }; + +const LectureSearchFilterMenu = (): JSX.Element => { + const { lectureInfoStore } = useStores(); + + const changeFilterStore = (type: string, value: string) => { + lectureInfoStore.changeFilterState(type, value); + }; + + const [selectState, onSelectMenuClick, { reset: resetSelectState }] = useSelectMenu(INIT_SELECT_STATE, { + callback: changeFilterStore, + }); + const { department, day, credit } = selectState; + + const [startTimeStr, isStartSelect, onStartTimeMenuClick, checkStartTimeSelectedItem, { reset: resetStartTime }] = useTimeSelectMenu({ + callback: changeFilterStore, + }); + + const [endTimeStr, isEndTimeSelect, onEndTimeMenuClick, checkEndTimeSelectedItem, { reset: resetEndTime }] = useTimeSelectMenu({ + callback: changeFilterStore, + }); + + const onInitButtonClickListener = () => { + lectureInfoStore.resetFilterState(); + resetSelectState(); + resetStartTime(); + resetEndTime(); + }; + + return ( + + ); +}; + +export { LectureSearchFilterMenu }; diff --git a/client/src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx b/src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx similarity index 73% rename from client/src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx rename to src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx index 218709e..276a82e 100644 --- a/client/src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx +++ b/src/components/UI/molecules/LoginModalContent/LoginModalContent.stories.tsx @@ -3,7 +3,7 @@ import { withKnobs } from '@storybook/addon-knobs'; import { Story, Meta } from '@storybook/react/types-6-0'; import { action } from '@storybook/addon-actions'; import { withStoryBox } from '@/components/HOC'; -import { LoginModalContent, LoginModalType, LoginModalContentProps } from './LoginModalContent'; +import { LoginModalContent, LoginModalContentProps } from './LoginModalContent'; export default { title: 'molecules/LoginModalContent', @@ -18,6 +18,8 @@ const Template: Story = (args) => { export const Default = Template.bind({}); Default.args = { - modalType: LoginModalType.LOGIN_MODAL, - onModalClose: action('onClick'), + isLoginDisabled: true, + onLoginBtnClick: action('onClick'), + onInputChange: action('onChange'), + onMovesignUpBtnClick: action('onClick'), }; diff --git a/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx b/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx new file mode 100644 index 0000000..fbabe7d --- /dev/null +++ b/src/components/UI/molecules/LoginModalContent/LoginModalContent.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { DialogTitle, DialogContent, DialogActions, TextField, Typography } from '@material-ui/core'; +import { Button, ButtonType } from '@/components/UI/atoms'; +import { makeStyles } from '@material-ui/core/styles'; + +enum LoginModalType { + LOGIN_MODAL = 'LOGIN_MODAL', +} + +interface LoginModalContentProps { + isLoginDisabled: boolean; + onLoginBtnClick: () => void; + onInputChange: () => void; + onMovesignUpBtnClick: () => void; +} + +const useStyles = makeStyles((theme) => ({ + title: { + display: 'flex', + justifyContent: 'center', + fontSize: '1.7rem', + color: theme.palette.primary.main, + }, + dialogContentRoot: { + '&.MuiDialogContent-root': { + overflow: 'hidden', + }, + }, + dialogActionRoot: { + display: 'flex', + flexDirection: 'column', + + '&.MuiDialogActions-root': { + padding: '1rem 1.5rem', + }, + }, + linkTextArea: { + display: 'flex', + justifyContent: 'flex-end', + width: '100%', + paddingTop: '1rem', + margin: '0 !important', + }, + linkText: { + color: theme.palette.grey[600], + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline', + cursor: 'pointer', + }, + }, +})); + +const LOGIN_BUTTON_STYLE_PROPS = { width: 192, height: 35.2, borderRadius: 4, fontSize: 16 }; + +const LoginModalContent = ({ isLoginDisabled, onLoginBtnClick, onInputChange, onMovesignUpBtnClick }: LoginModalContentProps): JSX.Element => { + const classes = useStyles(); + + return ( + <> + + 한표 로그인 + + + + + + + +
    + + 한표를 더 편리하게 이용하세요. 회원가입하기 + +
    +
    + + ); +}; + +export { LoginModalContent, LoginModalType }; +export type { LoginModalContentProps }; diff --git a/client/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.stories.tsx b/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.stories.tsx rename to src/components/UI/molecules/ModalPopupArea/ModalPopupArea.stories.tsx diff --git a/client/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.tsx b/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.tsx similarity index 100% rename from client/src/components/UI/molecules/ModalPopupArea/ModalPopupArea.tsx rename to src/components/UI/molecules/ModalPopupArea/ModalPopupArea.tsx diff --git a/src/components/UI/molecules/MyPageMemberInfo/MyPageMemberInfo.tsx b/src/components/UI/molecules/MyPageMemberInfo/MyPageMemberInfo.tsx new file mode 100644 index 0000000..957a2a4 --- /dev/null +++ b/src/components/UI/molecules/MyPageMemberInfo/MyPageMemberInfo.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Avatar, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { MY_MEMBER_INFO } from '@/queries'; +import client from '@/apollo'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + width: '50%', + }, + image: { + width: '12rem', + height: '12rem', + marginBottom: '2rem', + }, + nicknameText: { + color: theme.palette.grey[700], + }, + majorText: { + color: theme.palette.grey[500], + }, + box: { + border: `1px solid ${theme.palette.grey[400]}`, + borderRadius: '0.8rem', + padding: '0.5rem 1.5rem', + marginTop: '0.5rem', + }, +})); + +const MyPageMemberInfo = (): JSX.Element => { + const classes = useStyles(); + + const { myMemberInfo } = client.readQuery({ + query: MY_MEMBER_INFO, + }); + + return ( +
    + + + {myMemberInfo.nickname}님 + +
    + + {myMemberInfo.major} + +
    +
    + ); +}; + +export { MyPageMemberInfo }; diff --git a/src/components/UI/molecules/MyPageMenus/MyPageMenus.tsx b/src/components/UI/molecules/MyPageMenus/MyPageMenus.tsx new file mode 100644 index 0000000..30204e4 --- /dev/null +++ b/src/components/UI/molecules/MyPageMenus/MyPageMenus.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { MyPageMenu, MyPageMenuType } from '@/components/UI/atoms'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexDirection: 'column', + }, +})); + +const menus = [ + MyPageMenuType.MEMBER_INFO, + MyPageMenuType.CHANGE_PASSWORD, + MyPageMenuType.MY_POSTING, + MyPageMenuType.FRIEND_MANAGEMENT, + MyPageMenuType.SHARE, + MyPageMenuType.LOOKUP_CREDIT, + MyPageMenuType.WITHDRAWAL, +]; + +const MyPageMenus = () => { + const classes = useStyles(); + + const getMyPageMenus = () => { + return menus.map((menu) => { + return ; + }); + }; + + return
    {getMyPageMenus()}
    ; +}; + +export { MyPageMenus }; diff --git a/client/src/components/UI/molecules/Notice/Notice.stories.tsx b/src/components/UI/molecules/Notice/Notice.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/Notice/Notice.stories.tsx rename to src/components/UI/molecules/Notice/Notice.stories.tsx diff --git a/client/src/components/UI/molecules/Notice/Notice.tsx b/src/components/UI/molecules/Notice/Notice.tsx similarity index 100% rename from client/src/components/UI/molecules/Notice/Notice.tsx rename to src/components/UI/molecules/Notice/Notice.tsx diff --git a/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.stories.tsx b/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.stories.tsx new file mode 100644 index 0000000..b578119 --- /dev/null +++ b/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.stories.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { action } from '@storybook/addon-actions'; +import { withStoryBox } from '@/components/HOC'; +import { ReviewDetailModalContent, ReviewDetailModalType, ReviewDetailModalContentProps } from './ReviewDetailModalContent'; + +export default { + title: 'molecules/ReviewDetailModalContent', + component: ReviewDetailModalContent, + decorators: [withKnobs], +} as Meta; + +const mockData = { + id: 0, + infos: { + lectureName: '디자인커뮤니케이션', + profName: '윤정식', + rating: 3.5, + period: '2020년도 2학기', + }, + content: + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quos blanditiis tenetur unde suscipit, quam beatae rerum inventore consectetur, neque doloribus, cupiditate numquam dignissimos laborum fugiat deleniti? Eum quasi quidem quibusdam. 안녕하세요 제 이름은 지녀쿠입니다. 한기대학생이 아니지만 한표를 만들고 있답니다 하하', + tags: ['꿀수업', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠'], + scores: { + upScore: 20, + downScore: 3, + }, +}; + +const Template: Story = (args) => { + const ReviewDetailModalContentStory = withStoryBox(args, 700)(ReviewDetailModalContent); + return ; +}; + +export const OthersReviewDetail = Template.bind({}); +OthersReviewDetail.args = { + data: mockData, + modalType: ReviewDetailModalType.REVIEW_DETAIL_MODAL, + onModalClose: action('onClick'), + onModalModifyBtnClick: action('onClick'), + onModalDeleteBtnClick: action('onClick'), +}; + +export const MyReviewDetail = Template.bind({}); +MyReviewDetail.args = { + data: mockData, + modalType: ReviewDetailModalType.REVIEW_DETAIL_MODAL, + onModalClose: action('onClick'), + isMine: true, + onModalModifyBtnClick: action('onClick'), + onModalDeleteBtnClick: action('onClick'), +}; diff --git a/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.tsx b/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.tsx new file mode 100644 index 0000000..e525c4d --- /dev/null +++ b/src/components/UI/molecules/ReviewDetailModalContent/ReviewDetailModalContent.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { Button, DialogTitle, DialogContent, DialogActions, Typography, Divider } from '@material-ui/core'; +import Close from '@material-ui/icons/Close'; +import { makeStyles } from '@material-ui/core/styles'; +import { LectureReviewRating } from '@/components/UI/atoms'; +import { LectureReviewThumbs, LectureReviewHashTags } from '@/components/UI/molecules'; +import { LectureReviewData } from '@/components/UI/organisms'; + +enum ReviewDetailModalType { + REVIEW_DETAIL_MODAL = 'REVIEW_DETAIL_MODAL', +} + +interface ReviewDetailModalContentProps { + data: LectureReviewData; + isMine?: boolean; + onModalClose: () => void; + onModalModifyBtnClick: () => void; + onModalDeleteBtnClick: () => void; +} + +const useStyles = makeStyles((theme) => ({ + title: { + display: 'flex', + flexDirection: 'column', + }, + titleTopArea1: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + titleTopArea2: { + display: 'flex', + width: '40%', + justifyContent: 'space-between', + alignItems: 'center', + }, + normalFlexBox: { + display: 'flex', + }, + closeBtn: { + color: theme.palette.grey[400], + '&:hover': { + cursor: 'pointer', + opacity: 0.7, + }, + }, + action: { + display: 'flex', + flexDirection: 'column', + }, + actionThumbs: { + marginLeft: 'auto', + }, + actionButtons: { + display: 'flex', + width: '100%', + justifyContent: 'space-between', + }, + modifyButton: { + backgroundColor: theme.palette.primary.main, + width: '45%', + color: 'white', + borderRadius: '1rem', + '&:hover': { + backgroundColor: theme.palette.primary.main, + opacity: 0.7, + }, + }, + deleteButton: { + width: '45%', + backgroundColor: theme.palette.secondary.main, + borderRadius: '1rem', + '&:hover': { + backgroundColor: theme.palette.secondary.main, + opacity: 0.7, + }, + }, + divider: { + margin: '0 0.5rem', + }, +})); + +const ReviewDetailModalContent = ({ + data, + isMine = false, + onModalClose, + onModalModifyBtnClick, + onModalDeleteBtnClick, +}: ReviewDetailModalContentProps): JSX.Element => { + const classes = useStyles(); + + return ( + <> + +
    +
    + {data.infos.lectureName} + {data.infos.profName} +
    + +
    +
    + {data.infos.period} + + 작성 날짜 +
    +
    + 평점 + +
    +
    + +
    +
    + + {data.content} + + +
    + +
    + {isMine && ( +
    + + +
    + )} +
    + + ); +}; + +export { ReviewDetailModalContent, ReviewDetailModalType }; +export type { ReviewDetailModalContentProps }; diff --git a/src/components/UI/molecules/ReviewSearchSection/ReviewSearchSection.tsx b/src/components/UI/molecules/ReviewSearchSection/ReviewSearchSection.tsx new file mode 100644 index 0000000..dc7cd18 --- /dev/null +++ b/src/components/UI/molecules/ReviewSearchSection/ReviewSearchSection.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { FormControlLabel } from '@material-ui/core'; +import CheckBox from '@material-ui/core/Checkbox'; +import { SelectMenu } from '@/components/UI/atoms'; +import { ReviewSearchBar } from '@/components/UI/molecules'; +import { useSelectMenu } from '@/common/hooks'; + +enum OrderType { + LATEST = '최신 순', + RECOMMEND = '추천 순', + RATING = '별점 순', +} + +interface SelectMenuState { + order: string; +} + +const INIT_SELECT_STATE = { order: '' }; + +const INIT_ORDER_MENU = [ + { id: 0, title: OrderType.LATEST, value: OrderType.LATEST }, + { id: 1, title: OrderType.RECOMMEND, value: OrderType.RECOMMEND }, + { id: 2, title: OrderType.RATING, value: OrderType.RATING }, +]; + +const ReviewSearchSection = (): JSX.Element => { + const [isChecked, setIsChecked] = useState(false); + const [selectState, onSelectMenuClick] = useSelectMenu(INIT_SELECT_STATE); + const { order } = selectState; + + const onCheckBoxChangeHandler = (event: React.ChangeEvent) => { + setIsChecked(event.target.checked); + }; + + return ( + <> + + } + label="내가 쓴 글 보기" + /> + + + ); +}; + +export { ReviewSearchSection }; diff --git a/src/components/UI/molecules/SearchBar/LectureSearchBar.tsx b/src/components/UI/molecules/SearchBar/LectureSearchBar.tsx new file mode 100644 index 0000000..7a7bdc1 --- /dev/null +++ b/src/components/UI/molecules/SearchBar/LectureSearchBar.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useStores } from '@/stores'; +import { throttle } from '@/common/utils'; +import { SearchBar } from './SearchBar'; + +const LectureSearchBar = (): JSX.Element => { + const { lectureInfoStore } = useStores(); + + const onSearchBarChangeListener = throttle((e: React.ChangeEvent) => { + const { value } = e.target; + lectureInfoStore.setSearchWord(value); + }, 500); + + return ; +}; + +export { LectureSearchBar }; diff --git a/src/components/UI/molecules/SearchBar/ReviewSearchBar.tsx b/src/components/UI/molecules/SearchBar/ReviewSearchBar.tsx new file mode 100644 index 0000000..de3190d --- /dev/null +++ b/src/components/UI/molecules/SearchBar/ReviewSearchBar.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { throttle } from '@/common/utils'; +import { SearchBar, SearchBarProps } from './SearchBar'; + +interface ReviewSearchBarProps { + searchBarProp?: SearchBarProps; +} + +const ReviewSearchBar = ({ searchBarProp }: ReviewSearchBarProps): JSX.Element => { + const onSearchBarChangeListener = throttle((e: React.ChangeEvent) => { + const { value } = e.target; + }, 500); + + return ; +}; + +export { ReviewSearchBar }; diff --git a/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx b/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx new file mode 100644 index 0000000..200eb57 --- /dev/null +++ b/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { action } from '@storybook/addon-actions'; +import { SearchBar, SearchBarProps } from '@/components/UI/molecules'; + +export default { + title: 'molecules/SearchBar', + component: SearchBar, + decorators: [withKnobs], +} as Meta; + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + onSearchBarChange: action('onChange'), +}; diff --git a/src/components/UI/molecules/SearchBar/SearchBar.tsx b/src/components/UI/molecules/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..df92470 --- /dev/null +++ b/src/components/UI/molecules/SearchBar/SearchBar.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Search } from '@material-ui/icons'; + +interface SearchBarProps { + width?: string; + onSearchBarChange?: () => void; +} + +interface CSSProps { + width: string; +} + +const useStyles = makeStyles((theme) => ({ + root: ({ width }: CSSProps) => ({ + borderRadius: '1.25rem', + backgroundColor: `${theme.palette.grey[100]}`, + display: 'flex', + width: `${width}`, + boxSizing: 'border-box', + padding: `0.4rem 1.5rem`, + alignItems: 'center', + justifyContent: 'space-between', + }), + input: { + width: '85%', + border: 'none', + backgroundColor: 'rgba(0, 0, 0, 0)', + fontSize: '0.9rem', + + '&:focus': { + outline: 'none', + }, + }, + icon: { + color: `${theme.palette.grey[500]}`, + + '&:hover': { + cursor: 'pointer', + }, + }, +})); + +const SearchBar = ({ onSearchBarChange, width = '100%' }: SearchBarProps): JSX.Element => { + const classes = useStyles({ width }); + + return ( +
    + + +
    + ); +}; + +export { SearchBar }; +export type { SearchBarProps }; diff --git a/client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx b/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx similarity index 68% rename from client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx rename to src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx index 9f5d420..0cbf3d6 100644 --- a/client/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx +++ b/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.stories.tsx @@ -12,12 +12,20 @@ export default { } as Meta; const Template: Story = (args) => { - const SignUpModalContentStory = withStoryBox(args, 300)(SignUpModalContent); + const SignUpModalContentStory = withStoryBox(args, 700)(SignUpModalContent); return ; }; export const Default = Template.bind({}); Default.args = { modalType: SignUpModalType.SIGN_UP_MODAL, - onModalClose: action('onClick'), + valid: [true, true, true, true, true, true], + selectValue: { + gradeValue: '', + majorValue: '', + }, + isSignupDisabled: true, + onSignupBtnClick: action('onClick'), + onInputChange: action('onClick'), + onMoveLoginBtnClick: action('onClick'), }; diff --git a/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx b/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx new file mode 100644 index 0000000..22f43ab --- /dev/null +++ b/src/components/UI/molecules/SignUpModalContent/SignUpModalContent.tsx @@ -0,0 +1,361 @@ +import React from 'react'; +import { DialogTitle, DialogContent, DialogActions, TextField, Typography, MenuItem } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { Button, ButtonType } from '@/components/UI/atoms'; + +enum SignUpModalType { + SIGN_UP_MODAL = 'SIGN_UP_MODAL', +} + +interface SelectValue { + gradeValue: string; + majorValue: string; +} + +interface EmailCheckInfo { + email: string; + duplicated: boolean; + loading: boolean; + called: boolean; +} + +interface NicknameCheckInfo { + nickname: string; + duplicated: boolean; + loading: boolean; + called: boolean; +} + +interface SignUpModalContentProps { + valid: boolean[]; + selectValue: SelectValue; + emailCheckInfo: EmailCheckInfo; + nicknameCheckInfo: NicknameCheckInfo; + isSignupDisabled: boolean; + onInputChange: () => void; + onSignupBtnClick: () => void; + onMoveLoginBtnClick: () => void; + onCheckDuplicatedBtnClick: (type: string) => void; +} + +const useStyles = makeStyles((theme) => ({ + title: { + display: 'flex', + justifyContent: 'center', + fontSize: '1.7rem', + color: theme.palette.primary.main, + }, + checkArea: { + display: 'flex', + '& > *': { + marginRight: '0.5rem', + }, + '& > *:not(:first-child)': { + marginTop: '0.4375rem', + }, + }, + checkButton: { + marginTop: '0.4375rem', + }, + selectArea: { + display: 'flex', + '& > *': { + marginRight: '0.5rem', + }, + }, + dialogActionRoot: { + display: 'flex', + flexDirection: 'column', + + '&.MuiDialogActions-root': { + padding: '1rem 1.5rem', + }, + }, + linkTextArea: { + display: 'flex', + justifyContent: 'flex-end', + width: '100%', + paddingTop: '1rem', + margin: '0 !important', + }, + linkText: { + color: theme.palette.grey[600], + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline', + cursor: 'pointer', + }, + }, +})); + +const HELPER_TEXT = { + EMAIL: { + DEFAULT: '아이디는 한기대 포털 아이디입니다. @koreatech.ac.kr은 빼고 입력해주세요.', + SUCCESS: '사용가능한 아이디입니다.', + CHECK: '아이디 중복체크를 진행해주세요.', + CHECKING: '이메일 중복 체크 중입니다.', + CHECK_ERROR: '중복된 아이디입니다.', + ERROR: '한기대 포털 아이디는 1자리 이상 12자리 이하 / 영소문자, 숫자, _ 로만 조합되어야합니다. ', + }, + PASSWORD: { + DEFAULT: '비밀번호는 8자 이상 12자 이하 / 영문, 특수문자, 숫자 모두 최소 1개 포함해야합니다', + }, + NAME: { + DEFAULT: '이름은 최소 2자 이상 / 한글로만 조합되어야합니다.', + }, + NICKNAME: { + DEFAULT: '닉네임은 최소 1자리 이상 / 영문, 한글, 숫자로만 조합되어야합니다.', + SUCCESS: '사용가능한 닉네임입니다.', + CHECK: '닉네임 중복체크를 진행해주세요.', + CHECKING: '닉네임 중복 체크 중입니다.', + CHECK_ERROR: '중복된 닉네임입니다.', + }, + GRADE: { + DEFAULT: '학년을 선택해주세요.', + }, + MAJOR: { + DEFAULT: '전공을 선택해주세요.', + }, +}; + +const SIGNUP_BUTTON_STYLE_PROPS = { width: 192, height: 35.2, borderRadius: 4, fontSize: 16 }; +const CHECK_BUTTON_STYLE_PROPS = { width: 96, height: 40, borderRadius: 4, fontSize: 13 }; + +const GRADES = [1, 2, 3, 4]; + +const MAJORS = [ + '기계공학부', + '메카트로닉스공학부', + '전기전자통신공학부', + '컴퓨터공학부', + '디자인건축공학부', + '에너지신소재화학공학부', + '산업경영학부', + '교양학부', + 'HRD학과', + '융합학과', +]; + +const SignUpModalContent = ({ + valid, + emailCheckInfo, + nicknameCheckInfo, + selectValue, + isSignupDisabled, + onSignupBtnClick, + onInputChange, + onMoveLoginBtnClick, + onCheckDuplicatedBtnClick, +}: SignUpModalContentProps): JSX.Element => { + const classes = useStyles(); + const [isValidEmail, isValidPassword, isValidName, isValidNickname, isValidGrade, isValidMajor] = valid; + const { gradeValue, majorValue } = selectValue; + + const getGradeSelectOptions = (): JSX.Element[] => { + const gradeSelectOptions = GRADES.map((grade) => ( + + {grade} + + )); + + return gradeSelectOptions; + }; + + const getMajorSelectOptions = (): JSX.Element[] => { + const majorSelectOptions = MAJORS.map((major) => ( + + {major} + + )); + + return majorSelectOptions; + }; + + const onEmailCheckBtnClickListener = () => { + onCheckDuplicatedBtnClick('email'); + }; + + const onNicknameCheckBtnClickListener = () => { + onCheckDuplicatedBtnClick('nickname'); + }; + + const getEmailHelperMsg = (): string => { + const { email, duplicated, called, loading } = emailCheckInfo; + + if (email === '') return HELPER_TEXT.EMAIL.DEFAULT; + + if (!isValidEmail) return HELPER_TEXT.EMAIL.ERROR; + + if (!called) return HELPER_TEXT.EMAIL.CHECK; + + if (loading) return HELPER_TEXT.EMAIL.CHECKING; + + if (duplicated) return HELPER_TEXT.EMAIL.CHECK_ERROR; + + return HELPER_TEXT.EMAIL.SUCCESS; + }; + + const getNicknameHelperMsg = (): string => { + const { nickname, duplicated, called, loading } = nicknameCheckInfo; + + if (nickname === '') return HELPER_TEXT.NICKNAME.DEFAULT; + + if (!isValidNickname) return HELPER_TEXT.NICKNAME.DEFAULT; + + if (!called) return HELPER_TEXT.NICKNAME.CHECK; + + if (loading) return HELPER_TEXT.NICKNAME.CHECKING; + + if (duplicated) return HELPER_TEXT.NICKNAME.CHECK_ERROR; + + return HELPER_TEXT.NICKNAME.SUCCESS; + }; + + const checkEmailInputError = (): boolean => { + const { duplicated, loading, email, called } = emailCheckInfo; + return !!email && (!isValidEmail || duplicated || loading || !called); + }; + + const checkNicknameInputError = (): boolean => { + const { duplicated, loading, nickname, called } = nicknameCheckInfo; + return !!nickname && (!isValidNickname || duplicated || loading || !called); + }; + + const checkEmailCheckBtnDisabled = (): boolean => { + return !isValidEmail || !emailCheckInfo.email; + }; + + const checkNicknameCheckBtnDisabled = (): boolean => { + return !isValidNickname || !nicknameCheckInfo.nickname; + }; + + return ( + <> + + 한표 회원가입 + + +
    + + +
    + + +
    + + +
    +
    + + {getGradeSelectOptions()} + + + {getMajorSelectOptions()} + +
    +
    + + +
    + + 이미 가입하셨나요? 로그인하기 + +
    +
    + + ); +}; + +export { SignUpModalContent, SignUpModalType }; +export type { SignUpModalContentProps }; diff --git a/client/src/components/UI/molecules/SubTitle/SubTitle.stories.tsx b/src/components/UI/molecules/SubTitle/SubTitle.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/SubTitle/SubTitle.stories.tsx rename to src/components/UI/molecules/SubTitle/SubTitle.stories.tsx diff --git a/client/src/components/UI/molecules/SubTitle/SubTitle.tsx b/src/components/UI/molecules/SubTitle/SubTitle.tsx similarity index 100% rename from client/src/components/UI/molecules/SubTitle/SubTitle.tsx rename to src/components/UI/molecules/SubTitle/SubTitle.tsx diff --git a/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx b/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx new file mode 100644 index 0000000..708d995 --- /dev/null +++ b/src/components/UI/molecules/TimeTableAddForm/TimeTableAddForm.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { getTimeBoundByDay } from '@/common/utils'; +import { useStores } from '@/stores'; +import { useTimeSelectMenu, useSelectMenu, useInputForm } from '@/common/hooks'; +import { TimeTableAddFormContent } from './TimeTableAddFormContent'; + +interface SelectMenuState { + day: string; +} + +interface InputState { + schedule: string; +} + +const INIT_SELECT_STATE = { day: '' }; + +const INIT_INPUTS_STATE = { + schedule: '', +}; + +const DAY_MENU = [ + { id: 0, title: '월', value: 0 }, + { id: 1, title: '화', value: 1 }, + { id: 2, title: '수', value: 2 }, + { id: 3, title: '목', value: 3 }, + { id: 4, title: '금', value: 4 }, +]; + +const TimeTableAddForm = (): JSX.Element => { + const { timeTableStore, snackbarStore } = useStores(); + + const [selectState, onSelectMenuClick] = useSelectMenu(INIT_SELECT_STATE); + const { day } = selectState; + + const [startTimeStr, isStartSelect, onStartTimeMenuClick, checkStartTimeSelectedItem, { time: startTime }] = useTimeSelectMenu(); + const [endTimeStr, isEndTimeSelect, onEndTimeMenuClick, checkEndTimeSelectedItem, { time: endTime }] = useTimeSelectMenu(); + + const [inputs, onInputChange] = useInputForm(INIT_INPUTS_STATE); + const { schedule } = inputs; + + const onButtonClickListener = () => { + if (!day && !startTimeStr && !endTimeStr && schedule) return; + const dayBound = getTimeBoundByDay(day); + + const newCustomLecture = { + id: 0, + code: '', + name: schedule, + divisionNumber: ' ', + professor: '', + totalStudentNumber: '', + department: '나만의 스케줄', + lectureTimes: [{ start: startTime + dayBound.start, end: endTime + dayBound.start }], + room: '', + requiredGrade: '', + requiredMajor: '', + credit: '', + }; + + snackbarStore.setSnackbarType(timeTableStore.addLectureToTable(newCustomLecture)); + snackbarStore.setSnackbarState(true); + }; + + return ( + + ); +}; + +export { TimeTableAddForm }; diff --git a/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.stories.tsx b/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.stories.tsx rename to src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.stories.tsx diff --git a/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx b/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx similarity index 63% rename from client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx rename to src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx index 5024f38..497818b 100644 --- a/client/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx +++ b/src/components/UI/molecules/TimeTableAddForm/TimeTableAddFormContent.tsx @@ -4,9 +4,11 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { SelectMenu, TimeSelectMenu, SelectMenuProps, TimeSelectMenuProps, Button, ButtonType } from '@/components/UI/atoms'; interface TimeTableAddFormContentProps { - daySelectMenu: SelectMenuProps; - timeSelectMenu: TimeSelectMenuProps; - onTimeTableFormSubmit: () => void; + daySelectMenuProps: SelectMenuProps; + startTimeSelectMenuProps: TimeSelectMenuProps; + endTimeSelectMenuProps: TimeSelectMenuProps; + onInputChange: (event: React.ChangeEvent) => void; + onBtnClick: () => void; } const DROP_MENU_WIDTH = { @@ -14,6 +16,8 @@ const DROP_MENU_WIDTH = { TIME: '6.75rem', }; +const BUTTON_STYLE_PROPS = { width: 60, height: 32.25, borderRadius: 11.2, fontSize: 12 }; + const useStyles = makeStyles((theme: Theme) => createStyles({ root: { @@ -63,31 +67,41 @@ const useStyles = makeStyles((theme: Theme) => }), ); -const TimeTableAddFormContent = ({ daySelectMenu, timeSelectMenu, onTimeTableFormSubmit }: TimeTableAddFormContentProps): JSX.Element => { +const TimeTableAddFormContent = ({ + daySelectMenuProps, + startTimeSelectMenuProps, + endTimeSelectMenuProps, + onInputChange, + onBtnClick, +}: TimeTableAddFormContentProps): JSX.Element => { const classes = useStyles(); return ( -
    +
    - +
    - +
    - +
    - - + +
    ); }; diff --git a/client/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.stories.tsx b/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.stories.tsx rename to src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.stories.tsx diff --git a/client/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.tsx b/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.tsx rename to src/components/UI/molecules/TimeTableModalContent/TimeTableModalContent.tsx diff --git a/client/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.stories.tsx b/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.stories.tsx rename to src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.stories.tsx diff --git a/client/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.tsx b/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.tsx rename to src/components/UI/molecules/TimeTableTabBtnGroup/TimeTableTabBtnGroup.tsx diff --git a/client/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.stories.tsx b/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.stories.tsx rename to src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.stories.tsx diff --git a/client/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.tsx b/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.tsx similarity index 100% rename from client/src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.tsx rename to src/components/UI/molecules/TimeTableTabMenu/TimeTableTabMenu.tsx diff --git a/client/src/components/UI/molecules/Timetable/Timetable.stories.tsx b/src/components/UI/molecules/Timetable/Timetable.stories.tsx similarity index 100% rename from client/src/components/UI/molecules/Timetable/Timetable.stories.tsx rename to src/components/UI/molecules/Timetable/Timetable.stories.tsx diff --git a/client/src/components/UI/molecules/Timetable/Timetable.tsx b/src/components/UI/molecules/Timetable/Timetable.tsx similarity index 53% rename from client/src/components/UI/molecules/Timetable/Timetable.tsx rename to src/components/UI/molecules/Timetable/Timetable.tsx index d657f07..cd0cfd3 100644 --- a/client/src/components/UI/molecules/Timetable/Timetable.tsx +++ b/src/components/UI/molecules/Timetable/Timetable.tsx @@ -1,9 +1,9 @@ -/* eslint-disable no-continue */ import React from 'react'; import { LectureGrid } from '@/components/UI/atoms'; import { LectureBoxContainer } from '@/components/UI/molecules'; import { makeStyles } from '@material-ui/core/styles'; import { useStores } from '@/stores'; +import { range } from '@/common/utils'; interface TimetableProps { row: number; @@ -11,11 +11,11 @@ interface TimetableProps { } const useStyles = makeStyles((theme) => ({ - root: (props: TimetableProps) => ({ + root: ({ row, containedSat }: TimetableProps) => ({ display: 'grid', position: 'relative', - gridTemplateRows: `repeat(${props.row + 1}, 1fr)`, - gridTemplateColumns: `repeat(${props.containedSat ? 7 : 6}, 1fr)`, + gridTemplateRows: `repeat(${row + 1}, 1fr)`, + gridTemplateColumns: `repeat(${containedSat ? 7 : 6}, 1fr)`, width: '30rem', height: '44rem', border: `1px solid ${theme.palette.grey[300]}`, @@ -68,67 +68,60 @@ const days = [ { id: 6, name: 'Sat' }, ]; -interface dayType { - id: number; - name: string; -} - const Timetable = ({ row, containedSat }: TimetableProps): JSX.Element => { const classes = useStyles({ row, containedSat }); const { lectureInfoStore } = useStores(); - const fillTableHeader = () => { - // return days.reduce( - // (acc: JSX.Element | Element | Element[], day: dayType) => { - // if (!containedSat && day.name === 'Sat') { - // return acc; - // } - // return ( - // - // {day.name} - // - // ); - // }, - // [], - // ); - const array = [
    ]; - for (let i = 0; i < days.length; i += 1) { - const day = days[i]; - if (i === 5) { - if (!containedSat) continue; - } - array.push( -
    - {day.name} -
    , + + const getTableHeaders = (): JSX.Element[] => { + const headers = days.reduce( + (acc, day, idx) => { + if (idx === 5 && !containedSat) return acc; + + return acc.concat( +
    + {day.name} +
    , + ); + }, + [
    ], + ); + + return headers; + }; + + const getTimeGridInRow = (time: number): JSX.Element => { + if (time === 0) return
    {`0${time + 9}:00-${time + 10}:00`}
    ; + + if (time === 9) + return ( +
    + 이후 +
    ); - } - return array; + + return
    {`${time + 9}:00-${time + 10}:00`}
    ; }; - const makeRow = (time: number) => { - const column = containedSat ? 6 : 5; - const array = [...Array(column)].map((n, index) => { - return ; - }); - if (time === 0) { - array.unshift(
    {`0${time + 9}:00-${time + 10}:00`}
    ); - } else if (time === 9) { - array.unshift(
    이후
    ); - } else array.unshift(
    {`${time + 9}:00-${time + 10}:00`}
    ); - return array; + + const makeRowGrid = (time: number) => { + const columnCount = containedSat ? 6 : 5; + const rowGrid = [getTimeGridInRow(time)]; + + Array.from(range(1, columnCount)).forEach((key) => rowGrid.push()); + return rowGrid; }; - const fillTable = () => { - return [...Array(row)].map((n, index) => { - return makeRow(index); - }); + + const getTableGrid = () => { + return [...Array(row)].map((_, idx) => makeRowGrid(idx)); }; + const onMouseEnterListener = () => { - lectureInfoStore.state.selectedLecture(null); + lectureInfoStore.setSelectedLecture(null); }; return (
    - {fillTableHeader()} - {fillTable()} + {getTableHeaders()} + {getTableGrid()}
    ); diff --git a/client/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx b/src/components/UI/molecules/UserProfile/UserProfile.stories.tsx similarity index 58% rename from client/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx rename to src/components/UI/molecules/UserProfile/UserProfile.stories.tsx index 027145a..fa53d45 100644 --- a/client/src/components/UI/molecules/SearchBar/SearchBar.stories.tsx +++ b/src/components/UI/molecules/UserProfile/UserProfile.stories.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { withKnobs } from '@storybook/addon-knobs'; import { Story, Meta } from '@storybook/react/types-6-0'; -import { SearchBar } from '@/components/UI/molecules'; +import { UserProfile } from './UserProfile'; export default { - title: 'molecules/SearchBar', - component: SearchBar, + title: 'molecules/UserProfile', + component: UserProfile, decorators: [withKnobs], } as Meta; -const Template: Story = (args) => ; +const Template: Story = (args) => ; export const Default = Template.bind({}); diff --git a/src/components/UI/molecules/UserProfile/UserProfile.tsx b/src/components/UI/molecules/UserProfile/UserProfile.tsx new file mode 100644 index 0000000..5f4c29d --- /dev/null +++ b/src/components/UI/molecules/UserProfile/UserProfile.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Avatar, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { Link } from 'react-router-dom'; + +interface UserProfileProps { + nickname: string; + major: string; +} + +const useStyles = makeStyles((theme) => ({ + profileRoot: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + width: '12rem', + height: '5rem', + borderRadius: '0.65625rem', + backgroundColor: '#e9e9e9', + }, + + profileInfo: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginBottom: '0.3125rem', + + '& > *': { + marginRight: '0.5rem', + + '&:last-of-type': { + marginRight: 0, + }, + }, + }, + + profileTextArea: { + display: 'flex', + flexDirection: 'column', + + '& > *': { + marginBottom: '0.3125rem', + '&:last-child': { + marginBottom: 0, + }, + }, + + '& > span:first-child': { + color: theme.palette.grey[600], + fontSize: '0.9375rem', + }, + + '& > span:last-child': { + color: theme.palette.grey[500], + fontSize: '0.6875rem', + }, + }, + + divider: { + borderLeft: `1px solid ${theme.palette.grey[500]}`, + margin: '0 0.625rem', + }, + + menuRoot: { + display: 'flex', + justifyContent: 'center', + }, + + menuText: { + fontSize: '0.625rem', + + '& > a, &': { + color: theme.palette.grey[600], + textDecoration: 'none', + }, + + '& > a:hover, &:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline', + cursor: 'pointer', + }, + }, +})); + +const UserProfile = ({ nickname, major }: UserProfileProps): JSX.Element => { + const classes = useStyles(); + + return ( +
    +
    +
    + +
    +
    + {nickname} + {major} +
    +
    +
    + { + alert('로그아웃 구현예정입니다.'); + }}> + 로 그 아 웃 + +
    + + 마 이 페 이 지 + +
    +
    + ); +}; + +export { UserProfile }; diff --git a/client/src/components/UI/molecules/index.ts b/src/components/UI/molecules/index.ts similarity index 68% rename from client/src/components/UI/molecules/index.ts rename to src/components/UI/molecules/index.ts index 46fdcfd..9b3a821 100644 --- a/client/src/components/UI/molecules/index.ts +++ b/src/components/UI/molecules/index.ts @@ -1,22 +1,21 @@ export { Timetable } from './Timetable/Timetable'; export type { TimetableProps } from './Timetable/Timetable'; export { Notice } from './Notice/Notice'; -export { SearchBar } from './SearchBar/SearchBar'; +export { LectureSearchBar } from './SearchBar/LectureSearchBar'; +export { ReviewSearchBar } from './SearchBar/ReviewSearchBar'; export { LectureInfo } from './LectureInfo/LectureInfo'; export type { LectureInfoProps, LectureInfos, TimeTypes } from './LectureInfo/LectureInfo'; export { ModalPopupArea } from './ModalPopupArea/ModalPopupArea'; export type { ModalPopupAreaProps } from './ModalPopupArea/ModalPopupArea'; export { TimeTableModalContent, TimeTableModalType } from './TimeTableModalContent/TimeTableModalContent'; export type { TimeTableModalContentProps } from './TimeTableModalContent/TimeTableModalContent'; -export { HeaderAuthSection } from './HeaderAuthSection/HeaderAuthSection'; +export { HeaderLoginMenu } from './HeaderLoginMenu/HeaderLoginMenu'; export { SubTitle } from './SubTitle/SubTitle'; export { TimeTableTabBtnGroup } from './TimeTableTabBtnGroup/TimeTableTabBtnGroup'; export type { TimeTableTabBtnGroupProps } from './TimeTableTabBtnGroup/TimeTableTabBtnGroup'; export { TimeTableTabMenu } from './TimeTableTabMenu/TimeTableTabMenu'; export type { TimeTableTabMenuProps } from './TimeTableTabMenu/TimeTableTabMenu'; export { LectureBoxContainer } from './LectureBoxContainer/LectureBoxContainer'; -export { BasketLectureListBody } from './LectureListBody/BasketLectureListBody'; -export { SearchedLectureListBody } from './LectureListBody/SearchedLectureListBody'; export { LoginModalContent, LoginModalType } from './LoginModalContent/LoginModalContent'; export { LectureSearchFilterMenu } from './LectureSearchFilter/LectureSearchFilterMenu'; export { TimeTableAddForm } from './TimeTableAddForm/TimeTableAddForm'; @@ -26,3 +25,13 @@ export type { LectureReviewInfoProps } from './LectureReviewInfo/LectureReviewIn export { LectureReviewThumbs } from './LectureReviewThumbs/LectureReviewThumbs'; export type { LectureReviewThumbsProps } from './LectureReviewThumbs/LectureReviewThumbs'; export { LectureReviewHashTags } from './LectureReviewHashTags/LectureReviewHashTags'; +export { ReviewSearchSection } from './ReviewSearchSection/ReviewSearchSection'; +export { ReviewDetailModalContent, ReviewDetailModalType } from './ReviewDetailModalContent/ReviewDetailModalContent'; +export { LectureReviewTitle } from './LectureReviewTitle/LectureReviewTitle'; +export { MyPageMenus } from './MyPageMenus/MyPageMenus'; +export { LectureListContent } from './LectureListContent/LectureListContent'; +export { UserProfile } from './UserProfile/UserProfile'; +export { HeaderAuthSection } from './HeaderAuthSection/HeaderAuthSection'; +export { HeaderNavSection } from './HeaderNavSection/HeaderNavSection'; +export { FindModalContent, FindModalType } from './FindModalContent/FindModalContent'; +export { MyPageMemberInfo } from './MyPageMemberInfo/MyPageMemberInfo'; diff --git a/src/components/UI/organisms/Footer/Footer.stories.tsx b/src/components/UI/organisms/Footer/Footer.stories.tsx new file mode 100644 index 0000000..c9bfdf7 --- /dev/null +++ b/src/components/UI/organisms/Footer/Footer.stories.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { Footer } from '@/components/UI/organisms'; + +export default { + title: 'organisms/Footer', + component: Footer, + decorators: [withKnobs], +} as Meta; + +const Template: Story = (args) =>
    ; + +export const Default = Template.bind({}); diff --git a/src/components/UI/organisms/Footer/Footer.tsx b/src/components/UI/organisms/Footer/Footer.tsx new file mode 100644 index 0000000..68921d0 --- /dev/null +++ b/src/components/UI/organisms/Footer/Footer.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Typography, Link } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + marginTop: 'auto', + width: '100%', + height: '5rem', + backgroundColor: theme.palette.secondary.main, + }, + text: { + color: theme.palette.grey[500], + }, +})); + +const Footer = (): JSX.Element => { + const classes = useStyles(); + + return ( +
    + + GitHub + + + 피드백 + + + 개인정보처리방침 + + + 책임의 한계와 법적 고지 + + + Contact + +
    + ); +}; + +export { Footer }; diff --git a/client/src/components/UI/organisms/Header/Header.stories.tsx b/src/components/UI/organisms/Header/Header.stories.tsx similarity index 100% rename from client/src/components/UI/organisms/Header/Header.stories.tsx rename to src/components/UI/organisms/Header/Header.stories.tsx diff --git a/client/src/components/UI/organisms/Header/Header.tsx b/src/components/UI/organisms/Header/Header.tsx similarity index 75% rename from client/src/components/UI/organisms/Header/Header.tsx rename to src/components/UI/organisms/Header/Header.tsx index 73933e9..d1ba3e4 100644 --- a/client/src/components/UI/organisms/Header/Header.tsx +++ b/src/components/UI/organisms/Header/Header.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { Typography } from '@material-ui/core'; -import { HeaderMenu } from '@/components/UI/atoms'; import { makeStyles } from '@material-ui/core/styles'; - -import { HeaderAuthSection } from '@/components/UI/molecules'; +import { HeaderAuthSection, HeaderNavSection } from '@/components/UI/molecules'; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -37,9 +35,7 @@ const Header = (): JSX.Element => { 한표 - 시간표짜기 - 강의후기 - 마이페이지 +
    diff --git a/src/components/UI/organisms/LectureList/BasketLectureList.tsx b/src/components/UI/organisms/LectureList/BasketLectureList.tsx new file mode 100644 index 0000000..bfadd6a --- /dev/null +++ b/src/components/UI/organisms/LectureList/BasketLectureList.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { LectureInfos, LectureListContent } from '@/components/UI/molecules'; +import { useStores } from '@/stores'; +import { isString } from '@/common/utils/typeCheck'; +import { useReactiveVar } from '@apollo/client'; + +const BasketLectureList = () => { + const { timeTableStore, snackbarStore, lectureInfoStore } = useStores(); + + const savedLectures = useReactiveVar(timeTableStore.state.selectedTabLectures); + const selectedTabIdx = useReactiveVar(timeTableStore.state.selectedTabIdx); + const savedLecturesInSelectedTab = savedLectures[selectedTabIdx - 1]; + + const onBasketLectureDoubleClickListener = (lectureInfos: LectureInfos) => { + if (isString(lectureInfos.lectureTimes)) return; + + timeTableStore.removeLectureFromTable(lectureInfos.name); + snackbarStore.showTabDeleteMsg(); + }; + + const onBasketLectureClickListener = (lectureInfos: LectureInfos) => { + if (isString(lectureInfos.lectureTimes)) return; + + lectureInfoStore.setBasketSelectedLecture(lectureInfos); + }; + return ( + + ); +}; + +export { BasketLectureList }; diff --git a/client/src/components/UI/organisms/LectureList/LectureList.stories.tsx b/src/components/UI/organisms/LectureList/LectureList.stories.tsx similarity index 100% rename from client/src/components/UI/organisms/LectureList/LectureList.stories.tsx rename to src/components/UI/organisms/LectureList/LectureList.stories.tsx diff --git a/src/components/UI/organisms/LectureList/LectureList.tsx b/src/components/UI/organisms/LectureList/LectureList.tsx new file mode 100644 index 0000000..faba91a --- /dev/null +++ b/src/components/UI/organisms/LectureList/LectureList.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { SearchedLectureList, BasketLectureList } from '@/components/UI/organisms'; + +interface LectureListProps { + isBasket?: boolean; +} + +const LectureList = ({ isBasket = false }: LectureListProps): JSX.Element => { + if (isBasket) return ; + return ; +}; + +export { LectureList }; diff --git a/src/components/UI/organisms/LectureList/SearchedLectureList.tsx b/src/components/UI/organisms/LectureList/SearchedLectureList.tsx new file mode 100644 index 0000000..5b179c6 --- /dev/null +++ b/src/components/UI/organisms/LectureList/SearchedLectureList.tsx @@ -0,0 +1,139 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-shadow */ +import React from 'react'; +import { LectureInfos, LectureListContent } from '@/components/UI/molecules'; +import { useStores } from '@/stores'; +import { isString } from '@/common/utils/typeCheck'; +import { useQuery } from '@apollo/client'; +import { LectureListSkeleton } from '@/components/Skeleton'; +import { LECTURE_INFOS } from '@/queries'; +import { getTimeBoundByDay } from '@/common/utils'; +import { useReactiveVars } from '@/common/hooks'; + +interface LectureFilterState { + selectedDepartment: string | null; + selectedDay: string | null; + selectedCredit: string | null; + selectedStartTime: number | null; + selectedEndTime: number | null; + searchWord: string | null; +} + +const SearchedLectureList = (): JSX.Element => { + const { timeTableStore, snackbarStore, lectureInfoStore } = useStores(); + const { selectedDepartment, selectedDay, selectedCredit, selectedStartTime, selectedEndTime, searchWord } = useReactiveVars( + lectureInfoStore.getFilterState(), + ); + const { loading, error, data } = useQuery(LECTURE_INFOS); + + if (loading) { + return ( +
    + +
    + ); + } + + if (error) return

    Error :(

    ; + + const onLectureSearchClickListener = (lectureInfos: LectureInfos) => { + if (isString(lectureInfos.lectureTimes)) return; + + lectureInfoStore.setSelectedLecture(lectureInfos); + }; + + const filterBySearchWord = (lecturesInfos: LectureInfos[]) => { + const parseSearchWord = (searchWord: string): string[] => searchWord.replaceAll(' ', '').split(','); + const checkSearchWordInName = (parsedSearchWords: string[], name: string): boolean => { + if (!name) return false; + return parsedSearchWords.some((parsedSearchWord) => name.includes(parsedSearchWord)); + }; + + if (searchWord) { + const parsedSearchWords = parseSearchWord(searchWord); + + return lecturesInfos.filter((lectureInfo: LectureInfos) => { + const { name } = lectureInfo; + return checkSearchWordInName(parsedSearchWords, name); + }); + } + + return lecturesInfos; + }; + + const filterByDepartment = (lecturesInfos: LectureInfos[]): LectureInfos[] => { + if (selectedDepartment && selectedDepartment !== '전체') + return lecturesInfos.filter((lectureInfo: LectureInfos) => lectureInfo.department === selectedDepartment); + return lecturesInfos; + }; + + const filterByCredit = (lecturesInfos: LectureInfos[]): LectureInfos[] => { + if (selectedCredit && selectedCredit !== '전체') + return lecturesInfos.filter((lecturesInfo: LectureInfos) => lecturesInfo.credit === Number(selectedCredit[0])); + return lecturesInfos; + }; + + const filterByDay = (lecturesInfos: LectureInfos[]): LectureInfos[] => { + if (selectedDay && selectedDay !== '전체') { + return lecturesInfos.filter((lectureInfo: LectureInfos) => { + const { lectureTimes } = lectureInfo; + + if (isString(lectureTimes) || !lectureTimes) return false; + + return lectureTimes.some( + (lectureTime) => lectureTime.start >= getTimeBoundByDay(selectedDay).start && lectureTime.end < getTimeBoundByDay(selectedDay).end, + ); + }); + } + + return lecturesInfos; + }; + + const filterByTime = (lecturesInfos: LectureInfos[]): LectureInfos[] => { + if (selectedStartTime && selectedEndTime) { + return lecturesInfos.filter((lectureInfo: LectureInfos) => { + const { lectureTimes } = lectureInfo; + + if (isString(lectureTimes) || !lectureTimes) return false; + + return lectureTimes.some((lectureTime) => lectureTime.start % 1440 >= selectedStartTime && lectureTime.end % 1440 <= selectedEndTime); + }); + } + + return lecturesInfos; + }; + + const getFilteredLectures = () => { + if (!selectedDepartment && !selectedCredit && !selectedDay && !selectedStartTime && !selectedEndTime && !searchWord) return null; + + let filteredLectures = data.lectureInfos; + + filteredLectures = filterBySearchWord(filteredLectures); + filteredLectures = filterByDepartment(filteredLectures); + filteredLectures = filterByCredit(filteredLectures); + filteredLectures = filterByDay(filteredLectures); + filteredLectures = filterByTime(filteredLectures); + + return filteredLectures; + }; + + const onLectureSearchDoubleClickListener = (lectureInfos: LectureInfos) => { + if (isString(lectureInfos.lectureTimes)) return; + + timeTableStore.addLectureToTable(lectureInfos); + snackbarStore.showTabAddMsg(); // 이 부분은 리팩토링하면서 수정한 부분입니다! 적절하게 메소드를 변경해주세요! + + snackbarStore.setSnackbarType(timeTableStore.addLectureToTable(lectureInfos)); + snackbarStore.setSnackbarState(true); + }; + + return ( + + ); +}; + +export { SearchedLectureList }; diff --git a/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx b/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx new file mode 100644 index 0000000..9ca6769 --- /dev/null +++ b/src/components/UI/organisms/LectureReview/LectureReview.stories.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { withStoryBox } from '@/components/HOC'; +import { LectureReview, LectureReviewProps } from './LectureReview'; + +export default { + title: 'organisms/LectureReview', + component: LectureReview, + decorators: [withKnobs], +} as Meta; + +const mockData = { + id: 0, + infos: { + lectureName: '디자인커뮤니케이션', + profName: '윤정식', + rating: 3.5, + period: '2020년도 2학기', + }, + content: + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quos blanditiis tenetur unde suscipit, quam beatae rerum inventore consectetur, neque doloribus, cupiditate numquam dignissimos laborum fugiat deleniti? Eum quasi quidem quibusdam. 안녕하세요 제 이름은 지녀쿠입니다. 한기대학생이 아니지만 한표를 만들고 있답니다 하하', + tags: ['꿀수업', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠', '진혀쿠', '돔황챠'], + scores: { + upScore: 20, + downScore: 3, + }, +}; + +const Template: Story = (args) => { + const LectureReviewStory = withStoryBox(args, 800)(LectureReview); + return ; +}; + +export const MyReview = Template.bind({}); +MyReview.args = { + data: mockData, + isMine: true, +}; + +export const OthersReview = Template.bind({}); +OthersReview.args = { + data: mockData, +}; diff --git a/client/src/components/UI/organisms/LectureReview/LectureReview.tsx b/src/components/UI/organisms/LectureReview/LectureReview.tsx similarity index 58% rename from client/src/components/UI/organisms/LectureReview/LectureReview.tsx rename to src/components/UI/organisms/LectureReview/LectureReview.tsx index 98a04b0..09e764b 100644 --- a/client/src/components/UI/organisms/LectureReview/LectureReview.tsx +++ b/src/components/UI/organisms/LectureReview/LectureReview.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { LectureReviewInfo, @@ -8,12 +8,18 @@ import { LectureReviewThumbsProps, } from '@/components/UI/molecules'; -interface LectureReviewProps { +interface LectureReviewData { + id: number; infos: LectureReviewInfoProps; content: string; tags: string[]; scores: LectureReviewThumbsProps; +} + +interface LectureReviewProps { + data: LectureReviewData; isMine?: boolean; + onClick: (event: React.MouseEvent, ref: HTMLSpanElement | null) => void; } interface CSSProps { @@ -25,8 +31,9 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', - width: '40rem', - height: '9rem', + width: '100%', + height: '10rem', + boxSizing: 'border-box', border: `1px solid ${isMine ? theme.palette.primary.main : theme.palette.grey[300]}`, backgroundColor: isMine ? '#fdf6eb' : 'white', borderRadius: '0.5rem', @@ -58,22 +65,27 @@ const useStyles = makeStyles((theme) => ({ }, })); -const LectureReview = ({ infos, content, tags, scores, isMine = false }: LectureReviewProps): JSX.Element => { +const LectureReview = ({ data, isMine = false, onClick }: LectureReviewProps): JSX.Element => { const classes = useStyles({ isMine }); - + const lectureReview = useRef(null); return ( -
    + onClick(event, lectureReview.current)}>
    - - + +
    -
    {content}
    +
    {data.content}
    - +
    -
    + ); }; export { LectureReview }; -export type { LectureReviewProps }; +export type { LectureReviewProps, LectureReviewData }; diff --git a/src/components/UI/organisms/LectureReview/LectureReviewContainer.tsx b/src/components/UI/organisms/LectureReview/LectureReviewContainer.tsx new file mode 100644 index 0000000..949f711 --- /dev/null +++ b/src/components/UI/organisms/LectureReview/LectureReviewContainer.tsx @@ -0,0 +1,40 @@ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useReactiveVar } from '@apollo/client'; +import { useStores } from '@/stores'; +import { LectureReview } from './LectureReview'; + +const useStyles = makeStyles({ + root: { + width: '100%', + marginTop: '1.5rem', + }, +}); + +const LectureReviewContainer = (): JSX.Element => { + const classes = useStyles(); + const { lectureReviewStore, modalStore } = useStores(); + const reviews = useReactiveVar(lectureReviewStore.state.reviews); + + const onClickListener = (event: React.MouseEvent, ref: HTMLSpanElement | null) => { + if (!ref) return; + const { dataset } = ref; + lectureReviewStore.state.nowSelectedReviewId(Number(dataset?.id)); + modalStore.openLectureReviewDetailModal(); + }; + + const checkIsMine = () => { + // LectureReview의 writer와 서버로부터 받아온 내 닉네임을 비교하여 true/false 반환 + // 클라이언트에서 닉네임을 관리하게 될 경우 문제가 생길 수 있을 것 같음 + // 클라이언트에서 닉네임을 악의적으로 바꿔 글을 수정하는 것을 방지하기 위해 서버에서 한 번 더 검사하면 좋을 듯 + }; + const getLectureReviews = () => { + return reviews.map((review, idx) => { + return ; + }); + }; + return
    {getLectureReviews()}
    ; +}; + +export { LectureReviewContainer }; diff --git a/src/components/UI/organisms/LectureReviewWriteForm/LectureReviewWriteForm.tsx b/src/components/UI/organisms/LectureReviewWriteForm/LectureReviewWriteForm.tsx new file mode 100644 index 0000000..cc4ee8e --- /dev/null +++ b/src/components/UI/organisms/LectureReviewWriteForm/LectureReviewWriteForm.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { TextField } from '@material-ui/core'; +import { Button, ButtonType, SelectMenu, LectureReviewRating } from '@/components/UI/atoms'; +import { LectureReviewHashTags } from '@/components/UI/molecules'; +import { makeStyles } from '@material-ui/core/styles'; +import { useSelectMenu } from '@/common/hooks'; + +interface SelectMenuState { + semester: string; +} + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexDirection: 'column', + + '& > *': { + marginBottom: '0.9375rem', + '&:last-child': { + marginBottom: 0, + }, + }, + }, + flexArea: { + display: 'flex', + }, + textInputArea: { + '& > *': { + marginRight: '0.625rem', + '&:last-child': { + marginBottom: 0, + }, + }, + }, + textInput: { + '& > .MuiOutlinedInput-root': { + borderRadius: '1.875rem', + }, + }, + textArea: { + width: '38.5625rem', + height: '13.75rem', + padding: '0.625rem', + borderColor: theme.palette.grey[400], + borderRadius: '0.25rem', + resize: 'none', + outlineWidth: '0.25rem', + outlineColor: theme.palette.primary.main, + + '&:hover': { + borderColor: 'black', + }, + }, + selectArea: { + width: '12.5rem', + marginRight: '0.625rem', + }, + ratingArea: { + display: 'flex', + alignItems: 'center', + color: theme.palette.grey[600], + }, + tagArea: { + display: 'flex', + flexDirection: 'column', + padding: '0.625rem', + + '& > p': { + marginBottom: '0.3125rem', + color: theme.palette.grey[600], + fontSize: '0.8125rem', + }, + }, + btnArea: { + justifyContent: 'center', + + '& > *': { + marginRight: '1.25rem', + '&:last-child': { + marginBottom: 0, + }, + }, + }, +})); + +const INIT_SELECT_STATE = { semester: '' }; + +const MOCK_MENUS = [ + { id: 0, title: '1학기', value: 1 }, + { id: 1, title: '2학기', value: 2 }, +]; +const MOCK_TAGS = ['테스트', '스토리북', '진혀쿠']; + +const BUTTON_STYLE_PROPS = { width: 160, height: 36.3, borderRadius: 16, fontSize: 16 }; + +const LectureReviewWriteForm = (): JSX.Element => { + const classes = useStyles(); + const [selectState, onSelectMenuClick] = useSelectMenu(INIT_SELECT_STATE); + const { semester } = selectState; + + return ( +
    +
    + + +
    +
    +
    + +
    +
    + 평점 : + +
    +
    +
    +