diff --git a/package.json b/package.json index 7ba9d3c5..18ad95de 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "axios": "^1.4.0", "babel-plugin-styled-components": "^2.1.4", + "dayjs": "^1.11.9", "dotenv": "^16.3.1", "eslint": "^8.26.0", "eslint-config-next": "13.0.0", diff --git a/src/apis/error/throwAxiosError.ts b/src/apis/error/throwAxiosError.ts new file mode 100644 index 00000000..55dd15a5 --- /dev/null +++ b/src/apis/error/throwAxiosError.ts @@ -0,0 +1,17 @@ +import { isAxiosError } from "axios"; + +const throwAxiosError = (err: unknown) => { + if (!isAxiosError(err)) + return { + data: "", + status: "", + message: "", + }; + + const data = err?.response?.data; + const { code, status, message } = data; + + return { code, message, status }; +}; + +export default throwAxiosError; diff --git a/src/apis/httpClient/httpClient.ts b/src/apis/httpClient/httpClient.ts new file mode 100644 index 00000000..cdd33615 --- /dev/null +++ b/src/apis/httpClient/httpClient.ts @@ -0,0 +1,142 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +import { requestInterceptors, responseInterceptors } from "@/apis/interceptor"; +import Storage from "@/apis/storage"; +import refreshToken from "@/apis/token/refreshToken"; +import TOKEN from "@/global/constants/token.constant"; + +export interface HttpClientConfig { + baseURL?: string; + timeout?: number; + headers?: { Authorization?: string }; +} + +export class HttpClient { + private api: AxiosInstance; + + private static clientConfig: HttpClientConfig; + + constructor(url: string, axiosConfig: HttpClientConfig) { + this.api = axios.create({ + ...axiosConfig, + baseURL: `${axiosConfig.baseURL}${url}`, + }); + HttpClient.clientConfig = { headers: { Authorization: "" } }; + this.setting(); + } + + get(requestConfig?: AxiosRequestConfig) { + return this.api.get("", { ...HttpClient.clientConfig, ...requestConfig }); + } + + getById(requestConfig?: AxiosRequestConfig) { + return this.api.get("/:id", { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + getByTitle(url: string, requestConfig?: AxiosRequestConfig) { + return this.api.get(`/${url}`, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + getInQuery( + param: string, + data: string | number, + requestConfig?: AxiosRequestConfig, + ) { + return this.api.get(`?${param}=${data}`, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + post(data: unknown, requestConfig?: AxiosRequestConfig) { + return this.api.post("", data, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + postInQuery( + param: string, + data: unknown, + requestConfig?: AxiosRequestConfig, + ) { + return this.api.post(`?${param}=${data}`, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + put(data: unknown, requestConfig?: AxiosRequestConfig) { + return this.api.put("", data, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + putByTitle(title: string, data: unknown, requestConfig?: AxiosRequestConfig) { + return this.api.put(`/${title}`, data, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + delete(requestConfig?: AxiosRequestConfig) { + return this.api.delete("", { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + deleteById(id: number, requestConfig?: AxiosRequestConfig) { + return this.api.delete(`/${id}`, { + ...HttpClient.clientConfig, + ...requestConfig, + }); + } + + private setting() { + HttpClient.setCommonInterceptors(this.api); + // const queryClient = new QueryClient(); + + this.api.interceptors.response.use( + (response) => response, + (error) => { + // queryClient.invalidateQueries("getUser"); + refreshToken(); + return Promise.reject(error); + }, + ); + } + + static setAccessToken() { + const accessToken = Storage.getItem(TOKEN.ACCESS); + HttpClient.clientConfig.headers = { + ...HttpClient.clientConfig.headers, + Authorization: accessToken || undefined, + }; + } + + static removeAccessToken() { + Storage.setItem(TOKEN.ACCESS, ""); + } + + private static setCommonInterceptors(instance: AxiosInstance) { + instance.interceptors.request.use(requestInterceptors as never); + instance.interceptors.response.use(responseInterceptors); + } +} + +export const axiosConfig: HttpClientConfig = { + baseURL: process.env.NEXT_PUBLIC_BASE_URL, + timeout: 10000, +}; + +export default { + oauth: new HttpClient("api/auth/oauth/bsm", axiosConfig), + user: new HttpClient("api/user", axiosConfig), +}; diff --git a/src/apis/httpClient/index.ts b/src/apis/httpClient/index.ts index cb9c9fd0..23bfef63 100644 --- a/src/apis/httpClient/index.ts +++ b/src/apis/httpClient/index.ts @@ -1,129 +1,3 @@ -import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; -import { requestInterceptors, responseInterceptors } from "@/apis/interceptor"; -import Storage from "@/apis/storage"; -import refreshToken from "@/apis/token/refreshToken"; -import { QueryClient } from "react-query"; +import httpClient from "./httpClient"; -export interface HttpClientConfig { - baseURL?: string; - timeout?: number; - headers?: { Authorization?: string }; -} - -export class HttpClient { - private api: AxiosInstance; - - private static clientConfig: HttpClientConfig; - - constructor(url: string, axiosConfig: HttpClientConfig) { - this.api = axios.create({ - ...axiosConfig, - baseURL: `${axiosConfig.baseURL}${url}`, - }); - HttpClient.clientConfig = { headers: { Authorization: "" } }; - this.setting(); - } - - get(requestConfig?: AxiosRequestConfig) { - return this.api.get("", { ...HttpClient.clientConfig, ...requestConfig }); - } - - getById(requestConfig?: AxiosRequestConfig) { - return this.api.get("/:id", { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - getByTitle(url: string, requestConfig?: AxiosRequestConfig) { - return this.api.get(`/${url}`, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - getInQuery( - param: string, - data: string | number, - requestConfig?: AxiosRequestConfig, - ) { - return this.api.get(`?${param}=${data}`, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - post(data: unknown, requestConfig?: AxiosRequestConfig) { - return this.api.post("", data, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - put(data: unknown, requestConfig?: AxiosRequestConfig) { - return this.api.put("", data, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - putByTitle(title: string, data: unknown, requestConfig?: AxiosRequestConfig) { - return this.api.put(`/${title}`, data, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - delete(requestConfig?: AxiosRequestConfig) { - return this.api.delete("", { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - deleteById(id: number, requestConfig?: AxiosRequestConfig) { - return this.api.delete(`/${id}`, { - ...HttpClient.clientConfig, - ...requestConfig, - }); - } - - private setting() { - HttpClient.setCommonInterceptors(this.api); - const queryClient = new QueryClient(); - - this.api.interceptors.response.use( - (response) => response, - (error) => { - // const { status, code } = error.response.data - // if (status === 403 && code === exception.code.TOKEN_403_3) Storage.delItem('refresh_token') - Storage.delItem("access_token"); - queryClient.invalidateQueries("getUser"); - refreshToken(); - return Promise.reject(error); - }, - ); - } - - static setAccessToken() { - const accessToken = Storage.getItem("access_token"); - HttpClient.clientConfig.headers = { - ...HttpClient.clientConfig.headers, - Authorization: accessToken || undefined, - }; - } - - static removeAccessToken() { - Storage.setItem("access_token", ""); - } - - private static setCommonInterceptors(instance: AxiosInstance) { - instance.interceptors.request.use(requestInterceptors as any); - instance.interceptors.response.use(responseInterceptors); - } -} - -export const axiosConfig: HttpClientConfig = { - baseURL: process.env.NEXT_PUBLIC_BASE_URL, - timeout: 10000, -}; +export default httpClient; diff --git a/src/apis/interceptor/index.ts b/src/apis/interceptor/index.ts index 26556bed..c6a1882b 100644 --- a/src/apis/interceptor/index.ts +++ b/src/apis/interceptor/index.ts @@ -1,14 +1,15 @@ import Storage from "@/apis/storage"; import { AxiosRequestConfig, AxiosResponse } from "axios"; import refreshToken from "@/apis/token/refreshToken"; -import exception from "@/global/constants/exception.constant"; +import ERROR from "@/global/constants/error.constant"; +import TOKEN from "@/global/constants/token.constant"; export const requestInterceptors = (requestConfig: AxiosRequestConfig) => { - if (!Storage.getItem("access_token") && Storage.getItem("refresh_token")) + if (!Storage.getItem(TOKEN.ACCESS) && Storage.getItem(TOKEN.REFRESH)) refreshToken(); if (requestConfig.headers) { - requestConfig.headers.Authorization = Storage.getItem("access_token"); + requestConfig.headers.Authorization = Storage.getItem(TOKEN.ACCESS); } const urlParams = requestConfig.url?.split("/:") || []; @@ -31,7 +32,7 @@ export const requestInterceptors = (requestConfig: AxiosRequestConfig) => { }; export const responseInterceptors = (originalResponse: AxiosResponse) => { - if (originalResponse.status !== exception.status.SUCCESS) refreshToken(); + if (originalResponse.status !== ERROR.STATUS.SUCCESS) refreshToken(); return { ...originalResponse, diff --git a/src/apis/token/refreshToken.ts b/src/apis/token/refreshToken.ts index 66a35984..ea49fd33 100644 --- a/src/apis/token/refreshToken.ts +++ b/src/apis/token/refreshToken.ts @@ -1,16 +1,18 @@ import axios from "axios"; import Storage from "@/apis/storage"; +import TOKEN from "@/global/constants/token.constant"; const refreshToken = async () => { + // fix 필요 try { const res = ( await axios.put("/auth/refresh/access", { - refresh_token: Storage.getItem("refresh_token"), + refresh_token: Storage.getItem(TOKEN.REFRESH), }) ).data; - Storage.setItem("access_token", res.accessToken); + Storage.setItem(TOKEN.ACCESS, res.accessToken); } catch (err) { - Storage.delItem("refresh_token"); + Storage.delItem(TOKEN.REFRESH); } }; diff --git a/src/app/calender/page.tsx b/src/app/calender/page.tsx new file mode 100644 index 00000000..2b68f714 --- /dev/null +++ b/src/app/calender/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import CalenderPage from "@/page/calender"; + +const Calender = () => { + return ; +}; + +export default Calender; diff --git a/src/app/oauth/page.tsx b/src/app/oauth/page.tsx new file mode 100644 index 00000000..df61069a --- /dev/null +++ b/src/app/oauth/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import OAuthPage from "@/page/oauth"; + +const OAuth = () => { + return ; +}; + +export default OAuth; diff --git a/src/app/schedule/page.tsx b/src/app/schedule/page.tsx deleted file mode 100644 index b05d0087..00000000 --- a/src/app/schedule/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import SchedulePage from "@/page/schedule"; - -const Schedule = () => { - return ; -}; - -export default Schedule; diff --git a/src/app/timetable/page.tsx b/src/app/timetable/page.tsx new file mode 100644 index 00000000..f690aaab --- /dev/null +++ b/src/app/timetable/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import TimeTablePage from "@/page/timetable"; + +const TimeTable = () => { + return ; +}; + +export default TimeTable; diff --git a/src/components/atoms/ImageWithFallback.tsx b/src/components/atoms/ImageWithFallback.tsx index c3bb6644..25b91e1f 100644 --- a/src/components/atoms/ImageWithFallback.tsx +++ b/src/components/atoms/ImageWithFallback.tsx @@ -1,8 +1,8 @@ -import Image, { StaticImageData } from "next/image"; +import Image, { ImageProps, StaticImageData } from "next/image"; import { useState } from "react"; import styled, { css } from "styled-components"; -interface ImageWithFallbackProps { +interface ImageWithFallbackProps extends ImageProps { src: string; fallbackSrc: StaticImageData | string; alt: string; @@ -16,11 +16,13 @@ const ImageWithFallback = ({ alt, size, onErrorDelete, + ...props }: ImageWithFallbackProps) => { const [imgSrc, setImgSrc] = useState(src); return ( >; +} + +const Select = ({ + options, + defaultOption, + label, + size = "80px", + handler, +}: ISelectProps) => { + const [hover, setHover] = React.useState(true); + + const toggleHandler = (option: string) => { + handler(option); + setHover(true); + }; + return ( + setHover(false)} + onMouseLeave={() => setHover(true)} + > + + + {defaultOption} + + + + + {options.map((option) => ( + toggleHandler(option)} + > + {option} + + ))} + + + ); +}; + +const StyledSelect = styled.div` + width: fit-content; + cursor: pointer; +`; + +const StyledDefaultList = styled.div<{ size: string }>` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: ${({ size }) => size}; + padding: 6px 0; + border-radius: 4px; + background-color: ${color.white}; + box-shadow: 4px 4px 10px 0 rgba(144, 144, 144, 0.15); + + &:hover { + background-color: ${color.on_tertiary}; + } +`; + +const StyledDefaultListText = styled.div<{ label: string }>` + ${font.context}; + color: ${color.black}; + + &:after { + content: "${({ label }) => label}"; + } +`; + +const StyledList = styled.ul<{ hover: boolean }>` + position: absolute; + border-radius: 4px; + background-color: ${color.white}; + padding: 6px 4px; + + ${({ hover }) => + hover && + css` + display: none; + `} +`; + +const StyledListItem = styled.li<{ size: string; label: string }>` + width: calc(${({ size }) => size} - 8px); + padding: 6px 0 6px 8px; + border-radius: 4px; + ${font.context}; + background-color: ${color.white}; + + &:hover { + background-color: ${color.on_tertiary}; + } + + &:after { + content: "${({ label }) => label}"; + } +`; + +export default Select; diff --git a/src/components/common/Aside/InfomationBox.tsx b/src/components/common/Aside/InfomationBox.tsx index 9df42059..54967537 100644 --- a/src/components/common/Aside/InfomationBox.tsx +++ b/src/components/common/Aside/InfomationBox.tsx @@ -3,21 +3,54 @@ import { font } from "@/styles/font"; import React from "react"; import styled from "styled-components"; import Image from "next/image"; +import DefaultProfile from "@/global/assets/icons/profile_default.png"; +import { Student } from "@/global/types/user.type"; +import Link from "next/link"; +import ROUTER from "@/global/constants/router.constant"; +import USER from "@/global/constants/user.constant"; import Column from "../../Flex/Column"; import Row from "../../Flex/Row"; -const InfomationBox = () => { +interface IInfomationBoxProps { + user: Student; + isLogined: boolean; +} + +const InfomationBox = ({ user, isLogined }: IInfomationBoxProps) => { return ( - - - 2학년 2반 10번 - - 박우빈 - 학생 - - - + + {isLogined && user.role === USER.STUDENT && ( + <> + + + {user.student.grade}학년 + {user.student.classNo}반 + {user.student.studentNo}번 + + + {user.student.name} + {user.role} + + + 내 정보 + + )} + {!isLogined && ( + <> + 로그인이 필요합니다. + + 로그인 + + + )} ); }; @@ -33,7 +66,7 @@ const Container = styled.main` border-radius: 5px; `; -const UserGrade = styled.span` +const UserInfoText = styled.span` ${font.p3}; color: ${color.gray}; `; @@ -48,19 +81,17 @@ const UserType = styled.span` color: ${color.gray}; `; -const InfomationButton = styled.button` +const InfomationButton = styled(Link)` width: 76px; height: 26px; background-color: ${color.primary_blue}; border-radius: 5px; - display: block; + display: flex; + justify-content: center; + align-items: center; margin-left: auto; - - &:after { - ${font.btn3}; - color: ${color.white}; - content: "내 정보"; - } + ${font.btn3}; + color: ${color.white}; `; const ProfileImage = styled(Image)` @@ -69,4 +100,8 @@ const ProfileImage = styled(Image)` flex-shrink: 0; `; +const LoginText = styled.span` + ${font.context}; +`; + export default InfomationBox; diff --git a/src/components/common/Aside/MeisterBox.tsx b/src/components/common/Aside/MeisterBox.tsx index f14b0dbc..52eda935 100644 --- a/src/components/common/Aside/MeisterBox.tsx +++ b/src/components/common/Aside/MeisterBox.tsx @@ -2,17 +2,17 @@ import color from "@/styles/color"; import { font } from "@/styles/font"; import React from "react"; import styled from "styled-components"; -import service from "@/global/constants/service.constant"; +import SERVICE from "@/global/constants/service.constant"; import Row from "../../Flex/Row"; const scores = [ { - name: service.meister.name, - type: service.meister.type, + name: SERVICE.MEISTER.NAME, + type: SERVICE.MEISTER.TYPE, }, { - name: service.reward_points.name, - type: service.reward_points.type, + name: SERVICE.MEISTER.NAME, + type: SERVICE.MEISTER.TYPE, }, ]; diff --git a/src/components/common/Aside/assets/Check.tsx b/src/components/common/Aside/assets/Check.tsx new file mode 100644 index 00000000..e527ed30 --- /dev/null +++ b/src/components/common/Aside/assets/Check.tsx @@ -0,0 +1,41 @@ +import SVGAttribute from "@/global/types/SVGAttribute.type"; +import React from "react"; + +const Check = ({ width, height, color }: SVGAttribute) => { + return ( + + + + + + + ); +}; + +export default Check; diff --git a/src/components/common/Aside/assets/check.svg b/src/components/common/Aside/assets/check.svg deleted file mode 100644 index 7e1f10b7..00000000 --- a/src/components/common/Aside/assets/check.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/common/Aside/assets/right_arrow.svg b/src/components/common/Aside/assets/right_arrow.svg deleted file mode 100644 index edca5f4c..00000000 --- a/src/components/common/Aside/assets/right_arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/common/Aside/index.tsx b/src/components/common/Aside/index.tsx index 51afa90d..97e57d5a 100644 --- a/src/components/common/Aside/index.tsx +++ b/src/components/common/Aside/index.tsx @@ -1,18 +1,23 @@ import React from "react"; import styled from "styled-components"; import Row from "@/components/Flex/Row"; +import useUser from "@/hooks/useUser"; import InfomationBox from "./InfomationBox"; import MeisterBox from "./MeisterBox"; import JoinCheckBox from "./JoinCheckBox"; const Aside = () => { + const { user, isLogined } = useUser(); + return ( - - - - - + + {isLogined && ( + + + + + )} ); }; diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index 28d76339..1056213e 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import Logo from "@/global/assets/Logo"; +import Logo from "@/global/assets/svgs/Logo"; import color from "@/styles/color"; import Link from "next/link"; import Navigation from "./Navigation"; diff --git a/src/global/assets/icons/loading.gif b/src/global/assets/icons/loading.gif new file mode 100644 index 00000000..46786e24 Binary files /dev/null and b/src/global/assets/icons/loading.gif differ diff --git a/src/global/assets/icons/profile_default.png b/src/global/assets/icons/profile_default.png new file mode 100644 index 00000000..64fd9bee Binary files /dev/null and b/src/global/assets/icons/profile_default.png differ diff --git a/src/global/assets/svgs/Arrow.tsx b/src/global/assets/svgs/Arrow.tsx new file mode 100644 index 00000000..253ad6dd --- /dev/null +++ b/src/global/assets/svgs/Arrow.tsx @@ -0,0 +1,27 @@ +import SVGAttribute from "@/global/types/SVGAttribute.type"; +import React from "react"; + +const path = { + top: "M1.66419 23.4679C2.24222 24.0457 3.02609 24.3703 3.84342 24.3703C4.66075 24.3703 5.44462 24.0457 6.02265 23.4679L21.2803 8.21018L36.538 23.4679C37.1193 24.0293 37.898 24.34 38.7061 24.333C39.5143 24.326 40.2874 24.0018 40.8589 23.4303C41.4304 22.8588 41.7546 22.0857 41.7616 21.2775C41.7686 20.4694 41.4579 19.6907 40.8965 19.1094L23.4596 1.67249C22.8815 1.09464 22.0977 0.77002 21.2803 0.77002C20.463 0.77002 19.6791 1.09464 19.1011 1.67249L1.66419 19.1094C1.08634 19.6874 0.761719 20.4713 0.761719 21.2886C0.761719 22.106 1.08634 22.8898 1.66419 23.4679Z", + right: + "M1.36271 0.972786C0.784855 1.55081 0.460235 2.33468 0.460235 3.15201C0.460235 3.96934 0.784855 4.75321 1.36271 5.33124L16.6204 20.5889L1.36271 35.8466C0.801232 36.4279 0.490547 37.2066 0.49757 38.0147C0.504593 38.8229 0.828761 39.596 1.40026 40.1675C1.97175 40.739 2.74485 41.0632 3.55303 41.0702C4.36121 41.0772 5.13982 40.7665 5.72116 40.2051L23.1581 22.7681C23.7359 22.1901 24.0605 21.4063 24.0605 20.5889C24.0605 19.7716 23.7359 18.9877 23.1581 18.4097L5.72116 0.972786C5.14314 0.394932 4.35927 0.0703125 3.54194 0.0703125C2.7246 0.0703125 1.94074 0.394932 1.36271 0.972786Z", + bottom: + "M40.8592 1.67228C40.2812 1.09443 39.4973 0.769805 38.68 0.769805C37.8627 0.769805 37.0788 1.09443 36.5008 1.67228L21.2431 16.93L5.98543 1.67228C5.40409 1.1108 4.62548 0.800118 3.81729 0.80714C3.00911 0.814163 2.23601 1.13833 1.66452 1.70983C1.09302 2.28132 0.768859 3.05442 0.761836 3.8626C0.754813 4.67079 1.0655 5.44939 1.62697 6.03073L19.0639 23.4676C19.6419 24.0455 20.4258 24.3701 21.2431 24.3701C22.0604 24.3701 22.8443 24.0455 23.4223 23.4676L40.8592 6.03073C41.4371 5.45271 41.7617 4.66884 41.7617 3.85151C41.7617 3.03417 41.4371 2.25031 40.8592 1.67228Z", + left: "M23.1588 40.1678C23.7366 39.5898 24.0612 38.8059 24.0612 37.9886C24.0612 37.1713 23.7366 36.3874 23.1588 35.8094L7.9011 20.5517L23.1588 5.29402C23.7203 4.71268 24.0309 3.93407 24.0239 3.12589C24.0169 2.3177 23.6927 1.54461 23.1212 0.973112C22.5497 0.401617 21.7766 0.0774523 20.9685 0.0704294C20.1603 0.0634065 19.3817 0.374091 18.8003 0.935568L1.36341 18.3725C0.785557 18.9505 0.460938 19.7344 0.460938 20.5517C0.460938 21.369 0.785557 22.1529 1.36341 22.7309L18.8003 40.1678C19.3783 40.7457 20.1622 41.0703 20.9795 41.0703C21.7969 41.0703 22.5807 40.7457 23.1588 40.1678Z", +}; + +const Arrow = ({ width, height, color, direction }: SVGAttribute) => { + return ( + + + + ); +}; + +export default Arrow; diff --git a/src/global/assets/Logo.tsx b/src/global/assets/svgs/Logo.tsx similarity index 99% rename from src/global/assets/Logo.tsx rename to src/global/assets/svgs/Logo.tsx index aad0dcfc..9f9d1f6e 100644 --- a/src/global/assets/Logo.tsx +++ b/src/global/assets/svgs/Logo.tsx @@ -1,5 +1,5 @@ import React from "react"; -import SVGAttribute from "../types/SVGAttribute"; +import SVGAttribute from "../../types/SVGAttribute.type"; const Logo = ({ width, height, pointable }: SVGAttribute) => { return ( diff --git a/src/global/constants/exception.constant.ts b/src/global/constants/error.constant.ts similarity index 86% rename from src/global/constants/exception.constant.ts rename to src/global/constants/error.constant.ts index 96819dd5..1a121966 100644 --- a/src/global/constants/exception.constant.ts +++ b/src/global/constants/error.constant.ts @@ -1,5 +1,5 @@ -const exception = { - code: { +const ERROR = { + CODE: { IMG_400_1: "IMG-400-1", DOCS_404_1: "DOCS-404-1", DOCS_404_2: "DOCS-404-2", @@ -12,13 +12,13 @@ const exception = { TOKEN_403_3: "TOKEN-403-3", USER_404_1: "USER-404-1", }, - status: { + STATUS: { SUCCESS: 200, NOT_FOUND: 404, FORBIDDEN: 403, BAD_REQUEST: 400, SERVER_ERROR: 500, }, -}; +} as const; -export default exception; +export default ERROR; diff --git a/src/global/constants/key.constant.ts b/src/global/constants/key.constant.ts new file mode 100644 index 00000000..96a85f3c --- /dev/null +++ b/src/global/constants/key.constant.ts @@ -0,0 +1,5 @@ +const KEY = { + USER: "useUser", +}; + +export default KEY; diff --git a/src/global/constants/router.constant.ts b/src/global/constants/router.constant.ts new file mode 100644 index 00000000..c3f8ce2e --- /dev/null +++ b/src/global/constants/router.constant.ts @@ -0,0 +1,18 @@ +const ROUTER = { + HOME: "/", + MEISTER: "/meister", + APPLICATIONS: "/applications", + LOGIN: "/oauth", + LOSTFOUND: { + LIST: "/lostfound", + WRITE: "/lostfound/write", + }, + POST: { + LIST: "/post", + WRITE: "/post/write", + }, + TIMETABLE: "/timetable", + MYPAGE: "/mypage", +} as const; + +export default ROUTER; diff --git a/src/global/constants/service.constant.ts b/src/global/constants/service.constant.ts index b668792d..b73e71d0 100644 --- a/src/global/constants/service.constant.ts +++ b/src/global/constants/service.constant.ts @@ -1,12 +1,12 @@ -const service = { - meister: { - name: "마이스터역량인증제", - type: "meister", +const SERVICE = { + MEISTER: { + NAME: "마이스터역량인증제", + TYPE: "meister", }, - reward_points: { - name: "상벌점", - type: "reward_points", + REWARD_POINTS: { + NAME: "상벌점", + TYPE: "reward_points", }, -}; +} as const; -export default service; +export default SERVICE; diff --git a/src/global/constants/token.constant.ts b/src/global/constants/token.constant.ts new file mode 100644 index 00000000..375544d7 --- /dev/null +++ b/src/global/constants/token.constant.ts @@ -0,0 +1,6 @@ +const TOKEN = { + ACCESS: "access_token", + REFRESH: "refresh_token", +} as const; + +export default TOKEN; diff --git a/src/global/constants/user.constant.ts b/src/global/constants/user.constant.ts new file mode 100644 index 00000000..df3613ac --- /dev/null +++ b/src/global/constants/user.constant.ts @@ -0,0 +1,6 @@ +const USER = { + STUDENT: "STUDENT", + TEACHER: "TEACHER", +} as const; + +export default USER; diff --git a/src/global/helpers/profileMaker.helper.ts b/src/global/helpers/profileMaker.helper.ts new file mode 100644 index 00000000..ce6b66a3 --- /dev/null +++ b/src/global/helpers/profileMaker.helper.ts @@ -0,0 +1,5 @@ +const profileMaker = (userCode: number) => { + return `https://bssm.kro.kr/_next/image?url=https://auth.bssm.kro.kr/resource/user/profile/${userCode}.png&w=256&q=75`; +}; + +export default profileMaker; diff --git a/src/global/types/SVGAttribute.ts b/src/global/types/SVGAttribute.type.ts similarity index 59% rename from src/global/types/SVGAttribute.ts rename to src/global/types/SVGAttribute.type.ts index 70d88fb9..dcae4ad0 100644 --- a/src/global/types/SVGAttribute.ts +++ b/src/global/types/SVGAttribute.type.ts @@ -2,4 +2,6 @@ export default interface SVGAttribute { width?: number; height?: number; pointable?: boolean; + color?: string; + direction?: "top" | "right" | "bottom" | "left"; } diff --git a/src/global/types/modal.type.ts b/src/global/types/modal.type.ts new file mode 100644 index 00000000..82007666 --- /dev/null +++ b/src/global/types/modal.type.ts @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +export interface ModalStore { + title?: string; + content: ReactNode; + visible?: boolean; + menualClose?: boolean; + onClose?: () => void; +} diff --git a/src/global/types/user.type.ts b/src/global/types/user.type.ts new file mode 100644 index 00000000..ffd9f4b7 --- /dev/null +++ b/src/global/types/user.type.ts @@ -0,0 +1,31 @@ +interface AuthenticatedUser { + isLogin: true; + code: number; + nickname: string; + email: string; + level: number; + profile: string; + role: "STUDENT" | "TEACHER"; +} + +export interface Student extends AuthenticatedUser { + role: "STUDENT"; + student: { + name: string; + enrolledAt: number; + grade: number; + classNo: number; + studentNo: number; + }; +} + +export interface Teacher extends AuthenticatedUser { + role: "TEACHER"; + teacher: { + name: string; + }; +} + +export interface UnauthenticatedUser { + isLogin: boolean; +} diff --git a/src/hooks/useDate.ts b/src/hooks/useDate.ts new file mode 100644 index 00000000..dddb07eb --- /dev/null +++ b/src/hooks/useDate.ts @@ -0,0 +1,13 @@ +import dayjs from "dayjs"; + +const useDate = () => { + const getHMSDate = () => { + const date = dayjs(new Date()); + const HMSDate = dayjs(date).locale("ko").format("A h:mm:ss"); + return HMSDate; + }; + + return { getHMSDate }; +}; + +export default useDate; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts new file mode 100644 index 00000000..7bb20987 --- /dev/null +++ b/src/hooks/useModal.ts @@ -0,0 +1,30 @@ +import { ModalStore } from "@/global/types/modal.type"; +import modalStore from "@/store/modal.store"; +import { useCallback } from "react"; +import { useRecoilState } from "recoil"; + +const useModal = () => { + const [modal, setModal] = useRecoilState(modalStore); + + const openModal = useCallback( + (modalData: ModalStore) => { + setModal({ + ...modalData, + visible: true, + }); + }, + [setModal], + ); + + const closeModal = useCallback(() => { + setModal({ + title: "", + content: null, + visible: false, + }); + }, [setModal]); + + return { openModal, closeModal, visible: !!modal.visible }; +}; + +export default useModal; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts new file mode 100644 index 00000000..312c9dd9 --- /dev/null +++ b/src/hooks/useUser.ts @@ -0,0 +1,68 @@ +import httpClient, { HttpClient } from "@/apis/httpClient/httpClient"; +import Storage from "@/apis/storage"; +import React from "react"; +import KEY from "@/global/constants/key.constant"; +import TOKEN from "@/global/constants/token.constant"; +import { Student } from "@/global/types/user.type"; +import { emptyUser, userStore } from "@/store/user.store"; +import { useRouter } from "next/navigation"; +import { useQuery } from "react-query"; +import profileMaker from "@/global/helpers/profileMaker.helper"; +import { useRecoilState } from "recoil"; +import useWindow from "./useWindow"; +import useModal from "./useModal"; + +interface UseUserOptions { + authorizedPage?: boolean; +} + +const useUser = (options?: UseUserOptions) => { + const [user, setUser] = useRecoilState(userStore); + const router = useRouter(); + const { openModal, visible } = useModal(); + const { isWindow } = useWindow(); + + const { + data: userInfo, + remove, + isLoading, + } = useQuery( + [KEY.USER], + async () => { + HttpClient.setAccessToken(); + const { data } = await httpClient.user.get(); + const profile = profileMaker(data.code); + return { + ...data, + profile, + }; + }, + { enabled: !!Storage.getItem(TOKEN.ACCESS) }, + ); + + const logout = () => { + HttpClient.removeAccessToken(); + setUser(emptyUser); + remove(); + }; + + React.useEffect(() => { + if (userInfo) setUser(userInfo); + }, [setUser, userInfo]); + + React.useEffect(() => { + if (options?.authorizedPage && !isLoading && !userInfo && !visible) { + openModal({ + title: "로그인", + content: "로그인이 필요한 페이지입니다. 메인 페이지로 돌아갑니다.", + onClose: () => { + if (isWindow) router.push("/"); + }, + }); + } + }, [options, userInfo, isLoading, router, visible, openModal, isWindow]); + + return { user, isLogined: !!userInfo, logout }; +}; + +export default useUser; diff --git a/src/hooks/useWindow.ts b/src/hooks/useWindow.ts new file mode 100644 index 00000000..d891b4fa --- /dev/null +++ b/src/hooks/useWindow.ts @@ -0,0 +1,11 @@ +import { useEffect, useState } from "react"; + +export default function useWindow() { + const [isWindow, setIsWindow] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") setIsWindow(true); + }, []); + + return { isWindow }; +} diff --git a/src/page/applications/assets/LinkArrow.svg b/src/page/applications/assets/LinkArrow.svg deleted file mode 100644 index 1fdf803c..00000000 --- a/src/page/applications/assets/LinkArrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/page/applications/assets/LinkArrow.tsx b/src/page/applications/assets/LinkArrow.tsx new file mode 100644 index 00000000..afa433f4 --- /dev/null +++ b/src/page/applications/assets/LinkArrow.tsx @@ -0,0 +1,21 @@ +import SVGAttribute from "@/global/types/SVGAttribute.type"; +import React from "react"; + +const LinkArrow = ({ width, height, color }: SVGAttribute) => { + return ( + + + + ); +}; + +export default LinkArrow; diff --git a/src/page/applications/layouts/AppList.tsx b/src/page/applications/layouts/AppList.tsx index 63f27a25..f6f3b73a 100644 --- a/src/page/applications/layouts/AppList.tsx +++ b/src/page/applications/layouts/AppList.tsx @@ -1,6 +1,5 @@ import React from "react"; import styled from "styled-components"; -import AppListFilter from "./AppListFilter"; import AppListItem from "./AppListItem"; const AppList = () => { diff --git a/src/page/applications/layouts/AppListItem.tsx b/src/page/applications/layouts/AppListItem.tsx index b7830e3c..d733b584 100644 --- a/src/page/applications/layouts/AppListItem.tsx +++ b/src/page/applications/layouts/AppListItem.tsx @@ -5,8 +5,7 @@ import { font } from "@/styles/font"; import Link from "next/link"; import React from "react"; import styled from "styled-components"; -import Arrow from "@/page/applications/assets/LinkArrow.svg"; -import Image from "next/image"; +import LinkArrow from "@/page/applications/assets/LinkArrow"; const AppListItem = () => { return ( @@ -16,7 +15,7 @@ const AppListItem = () => { BSM Deploy - arrow + diff --git a/src/page/schedule/index.tsx b/src/page/calender/index.tsx similarity index 89% rename from src/page/schedule/index.tsx rename to src/page/calender/index.tsx index 21679469..d54c482f 100644 --- a/src/page/schedule/index.tsx +++ b/src/page/calender/index.tsx @@ -3,7 +3,7 @@ import React from "react"; import styled from "styled-components"; import ScheduleBox from "./layouts/ScheduleBox"; -const SchedulePage = () => { +const CalenderPage = () => { return ( @@ -27,4 +27,4 @@ const Container = styled.div` gap: 8px; `; -export default SchedulePage; +export default CalenderPage; diff --git a/src/page/schedule/layouts/CalenderList.tsx b/src/page/calender/layouts/CalenderList.tsx similarity index 100% rename from src/page/schedule/layouts/CalenderList.tsx rename to src/page/calender/layouts/CalenderList.tsx diff --git a/src/page/schedule/layouts/CalenderListItem.tsx b/src/page/calender/layouts/CalenderListItem.tsx similarity index 100% rename from src/page/schedule/layouts/CalenderListItem.tsx rename to src/page/calender/layouts/CalenderListItem.tsx diff --git a/src/page/schedule/layouts/DateBox.tsx b/src/page/calender/layouts/DateBox.tsx similarity index 100% rename from src/page/schedule/layouts/DateBox.tsx rename to src/page/calender/layouts/DateBox.tsx diff --git a/src/page/schedule/layouts/ScheduleBox.tsx b/src/page/calender/layouts/ScheduleBox.tsx similarity index 100% rename from src/page/schedule/layouts/ScheduleBox.tsx rename to src/page/calender/layouts/ScheduleBox.tsx diff --git a/src/page/schedule/layouts/WeekBox.tsx b/src/page/calender/layouts/WeekBox.tsx similarity index 100% rename from src/page/schedule/layouts/WeekBox.tsx rename to src/page/calender/layouts/WeekBox.tsx diff --git a/src/page/forum-post/assets/CategoryArrow.tsx b/src/page/forum-post/assets/CategoryArrow.tsx index e24310cf..0023c32b 100644 --- a/src/page/forum-post/assets/CategoryArrow.tsx +++ b/src/page/forum-post/assets/CategoryArrow.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const CategoryArrow = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/Check.tsx b/src/page/forum-post/assets/Check.tsx index d84ee2d1..7a6fb4e4 100644 --- a/src/page/forum-post/assets/Check.tsx +++ b/src/page/forum-post/assets/Check.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const Check = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/CommentIcon.tsx b/src/page/forum-post/assets/CommentIcon.tsx index e6638347..6f2934cd 100644 --- a/src/page/forum-post/assets/CommentIcon.tsx +++ b/src/page/forum-post/assets/CommentIcon.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const CommentIcon = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/Emoji.tsx b/src/page/forum-post/assets/Emoji.tsx index 8d0faa21..a875e271 100644 --- a/src/page/forum-post/assets/Emoji.tsx +++ b/src/page/forum-post/assets/Emoji.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const Emoji = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/Like.tsx b/src/page/forum-post/assets/Like.tsx index 968e4342..d0d08d9b 100644 --- a/src/page/forum-post/assets/Like.tsx +++ b/src/page/forum-post/assets/Like.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const Like = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/Time.tsx b/src/page/forum-post/assets/Time.tsx index 93a47f88..ef099f63 100644 --- a/src/page/forum-post/assets/Time.tsx +++ b/src/page/forum-post/assets/Time.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const Time = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum-post/assets/View.tsx b/src/page/forum-post/assets/View.tsx index 4c734270..6e961f5c 100644 --- a/src/page/forum-post/assets/View.tsx +++ b/src/page/forum-post/assets/View.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const View = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/forum/adaptors/index.tsx b/src/page/forum/adaptors/index.tsx deleted file mode 100644 index 8a585fb1..00000000 --- a/src/page/forum/adaptors/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { HttpClient, axiosConfig } from "@/apis/httpClient"; - -export default { - forum: new HttpClient("/forum", axiosConfig), -}; diff --git a/src/page/forum/assets/LikeLogo.tsx b/src/page/forum/assets/LikeLogo.tsx index 3b30b3f5..1f509f1e 100644 --- a/src/page/forum/assets/LikeLogo.tsx +++ b/src/page/forum/assets/LikeLogo.tsx @@ -1,4 +1,4 @@ -import SVGAttribute from "@/global/types/SVGAttribute"; +import SVGAttribute from "@/global/types/SVGAttribute.type"; import React from "react"; const LikeLogo = ({ width, height, pointable }: SVGAttribute) => { diff --git a/src/page/home/adaptors/.gitkeep b/src/page/home/adaptors/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/page/oauth/index.tsx b/src/page/oauth/index.tsx new file mode 100644 index 00000000..782d2f10 --- /dev/null +++ b/src/page/oauth/index.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import { useSearchParams } from "next/navigation"; +import React from "react"; +import Loading from "@/global/assets/icons/loading.gif"; +import styled from "styled-components"; +import { font } from "@/styles/font"; +import Column from "@/components/Flex/Column"; +import color from "@/styles/color"; +import { useLoginMutation } from "./services/mutations.service"; + +const OAuthPage = () => { + const authCode = useSearchParams().get("code"); + const loginMutation = useLoginMutation({ authCode }); + + React.useEffect(() => { + loginMutation.mutate(); + }, []); + + return ( + + + loading... + 로그인 처리중... + + + ); +}; + +const Container = styled.div` + width: 100%; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; +`; + +const LoadingText = styled.span` + ${font.H4}; + color: ${color.blue}; +`; + +export default OAuthPage; diff --git a/src/page/oauth/services/api.service.ts b/src/page/oauth/services/api.service.ts new file mode 100644 index 00000000..1dc61f28 --- /dev/null +++ b/src/page/oauth/services/api.service.ts @@ -0,0 +1,12 @@ +import httpClient from "@/apis/httpClient"; + +export interface ILoginParams { + authCode: string | null; +} + +export const login = async ({ authCode }: ILoginParams) => { + if (!authCode) return; + + const { data } = await httpClient.oauth.postInQuery("code", authCode); + return data; +}; diff --git a/src/page/oauth/services/mutations.service.ts b/src/page/oauth/services/mutations.service.ts new file mode 100644 index 00000000..a60f3b59 --- /dev/null +++ b/src/page/oauth/services/mutations.service.ts @@ -0,0 +1,25 @@ +import throwAxiosError from "@/apis/error/throwAxiosError"; +import Storage from "@/apis/storage"; +import ROUTER from "@/global/constants/router.constant"; +import TOKEN from "@/global/constants/token.constant"; +import { useRouter } from "next/navigation"; +import { useMutation } from "react-query"; +import { ILoginParams, login } from "./api.service"; + +export const useLoginMutation = ({ authCode }: ILoginParams) => { + const router = useRouter(); + + return useMutation(() => login({ authCode }), { + onSuccess: ({ data }) => { + const { accessToken, refreshToken } = data; + + Storage.setItem(TOKEN.ACCESS, accessToken); + Storage.setItem(TOKEN.REFRESH, refreshToken); + router.push(ROUTER.HOME); + }, + onError: (err) => { + console.log(err); + throwAxiosError(err); + }, + }); +}; diff --git a/src/page/timetable/index.tsx b/src/page/timetable/index.tsx new file mode 100644 index 00000000..2dff697b --- /dev/null +++ b/src/page/timetable/index.tsx @@ -0,0 +1,30 @@ +import Aside from "@/components/common/Aside"; +import React from "react"; +import styled from "styled-components"; +import TimteTableBox from "./layouts/TimteTableBox"; + +const TimeTablePage = () => { + return ( + + + +