diff --git a/.github/workflows/gpt_review.yml b/.github/workflows/gpt_review.yml deleted file mode 100644 index 75aa3cf85..000000000 --- a/.github/workflows/gpt_review.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Code Review - -permissions: - contents: read - pull-requests: write - -on: - pull_request: - types: [opened, reopened, synchronize] - -jobs: - test: - if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} - runs-on: ubuntu-latest - steps: - - uses: anc95/ChatGPT-CodeReview@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANGUAGE: Korean diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..ac47c31bd --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2023 Keeper Homepage Renewal2 Frontend Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 0971fc106..edca52591 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,135 @@ -# Homepage-Front-R2 -키퍼 홈페이지 프론트엔드 리뉴얼2 +
+ keeper logo +
+ +#
Homepage-Front-R2
+ +> 키퍼 홈페이지 프론트엔드 리뉴얼2 +> +> 개발기간 : 2023. 11. 11. ~ 2023. 10. 15. + +## 목차 + +[🔗 배포 주소](#-배포-주소)
+[👨‍💻 구성원](#-구성원)
+[👀 프로젝트 배경](#-프로젝트-배경)
+[🚀 프로젝트 목적](#-프로젝트-목적)
+[🔧 기술 스택](#-기술-스택)
+[💡 시작 가이드](#-시작-가이드)
+[💎 라이센스](#-라이센스)
+ +## 🔗 배포 주소 + +> 운영 서버 주소 : https://keeper.or.kr/ +> +> 개발 서버 주소 : https://dev.keeper.or.kr/ + +## 👨‍💻 구성원 + +### 🕹️ 주담당자 + + + + + + +
+ + main manager +
publdaze +
+
+
  • 이메일 문의
  • +
  • publdaze@naver.com
  • +
    + +### 🛠️ 유지 보수 담당자 + + + + + + + +
    + + publdaze +
    + publdaze +
    +
    +
    + + jasper200207 +
    + jasper200207 +
    +
    +
    + + pipisebastian +
    + pipisebastian +
    +
    +
    + +### 👨‍👨‍👦‍👦 기여자 + +[![contributors](https://contrib.rocks/image?repo=KEEPER31337/Homepage-Front-R2)](https://github.com/KEEPER31337/Homepage-Front-R2/graphs/contributors) + +## 👀 프로젝트 배경 + +#### 기존 리뉴얼 홈페이지의 문제점 + +- **통일성 부족한 디자인** + 각 페이지마다 개별적인 디자인으로 인해 통일성이 부족했습니다. +- **명확하지 않은 코딩 규칙** + 명확한 코딩 규칙이 없어 코드마다 다양한 코딩 스타일이 혼재되었습니다. +- **부족한 코드 리뷰** + 리뷰 없이 머지가 가능하여 코드 리뷰 활동이 활발히 이루어지지 않았습니다. +- **팀원 간 소통 부족** + 회의 및 업무 공유가 부족하여 다른 팀원들의 작업 현황을 파악하기 어려웠습니다. + +## 🚀 프로젝트 목적 + +- **명확한 기획과 디자인** + 기획 담당과 디자인 담당을 명확히 정하고, 분야별 회의를 진행하여 통일성을 유지하고자 합니다. +- **클린 코드** + Eslint와 Prettier를 명확하게 정의하여 코드의 일관성을 높이고, 코드 리뷰를 적극적으로 수행하여 상호간에 놓친 부분을 보완하여 클린 코드를 유지하고자 합니다. +- **가시적인 진척도와 활발한 소통** + 주기적인 업무 공유 및 회의를 통해 팀원 간 의견을 조율하고, GitHub의 프로젝트 기능을 활용하여 전체적인 프로젝트 진행 상황과 개별 진척도를 명확히 파악할 수 있도록 합니다. +- **코드 재사용성 높이기** + 공통된 기능을 수행하는 컴포넌트를 개발하여 코드의 재사용성을 높입니다. 이를 통해 일관된 디자인을 유지하고 유지보수의 효율성을 향상시킬 수 있습니다. + +## 🔧 기술 스택 + +### 🛠 Skill & Tool + + + +### 🧩 Communication Tool + + + +## 💡 시작 가이드 + +- nvm 설정 + ``` + nvm install + nvm use + ``` +- root path에 `.env` 파일 추가 + ``` + REACT_APP_API_URL={{ apiUrl }} + HTTPS=true + ``` +- 필요한 패키지 설치 및 실행 + ``` + npm install + npm start + ``` + +## 💎 라이센스 + +해당 프로젝트는 [MIT LICENSE](https://github.com/KEEPER31337/Homepage-Front-R2/blob/develop/LICENSE) 를 따릅니다. diff --git a/src/api/authApi.ts b/src/api/authApi.ts index 6052462f0..570ccba05 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -1,19 +1,28 @@ import { useMutation } from 'react-query'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { useSetRecoilState } from 'recoil'; import memberState from '@recoil/member.recoil'; const useSignOutMutation = () => { - const fetcher = () => axios.post(`/sign-out`); - const navigate = useNavigate(); const setMemberState = useSetRecoilState(memberState); + const fetcher = () => axios.post(`/sign-out`); + const signOut = () => { + navigate('/'); + setMemberState(null); + }; + return useMutation(fetcher, { onSuccess: () => { - navigate('/'); - setMemberState(null); + signOut(); + }, + onError: (error) => { + const errorStatusCode = (error as AxiosError).response?.status; + if (errorStatusCode === 401) { + signOut(); + } }, }); }; diff --git a/src/api/memberApi.ts b/src/api/memberApi.ts index 93a5dec59..ded70c331 100644 --- a/src/api/memberApi.ts +++ b/src/api/memberApi.ts @@ -1,6 +1,7 @@ import toast from 'react-hot-toast'; import { useQuery, useMutation, useQueryClient } from 'react-query'; import axios from 'axios'; +import { PASSWORD } from '@constants/apiResponseMessage'; import { useApiError } from '@hooks/useGetApiError'; import { formatGeneration } from '@utils/converter'; import { ProfileInfo, MemberDetailInfo } from './dto'; @@ -123,7 +124,7 @@ const useEditEmailMutation = () => { const { handleError } = useApiError({ 400: { default: () => { - toast.error('현재 비밀번호가 일치하지 않습니다.'); + toast.error(PASSWORD.error.mismatch); }, }, }); @@ -138,7 +139,7 @@ const useEditPasswordMutation = () => { const { handleError } = useApiError({ 400: { default: () => { - toast.error('현재 비밀번호가 일치하지 않습니다.'); + toast.error(PASSWORD.error.mismatch); }, }, }); @@ -147,7 +148,7 @@ const useEditPasswordMutation = () => { return useMutation(fetcher, { onSuccess: () => { - toast.success('비밀번호가 변경되었습니다.'); + toast.success(PASSWORD.success.changed); }, onError: (err) => handleError(err, 400), }); @@ -157,7 +158,7 @@ const useWithdrawalMutation = () => { const { handleError } = useApiError({ 400: { default: () => { - toast.error('비밀번호가 일치하지 않습니다.'); + toast.error(PASSWORD.error.mismatch); }, }, }); diff --git a/src/api/postApi.ts b/src/api/postApi.ts index f3d08cad7..905c57370 100644 --- a/src/api/postApi.ts +++ b/src/api/postApi.ts @@ -2,6 +2,7 @@ import toast from 'react-hot-toast'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useLocation, useNavigate } from 'react-router-dom'; import axios, { AxiosError } from 'axios'; +import { BOARD } from '@constants/apiResponseMessage'; import { useApiError } from '@hooks/useGetApiError'; import { BoardPosts, @@ -151,12 +152,12 @@ const useGetEachPostQuery = ( 400: { default: () => { // TODO 페이지 문구로 띄워주기 - toast.error('게시글 열람 조건을 충족하지 않습니다.'); + toast.error(BOARD.error.readCondition); }, }, 403: { 40301: () => { - toast.error('게시글의 비밀번호가 일치하지 않습니다.'); + toast.error(BOARD.error.mismatchPassword); }, 40302: () => { // 비밀글 여부 true로 변경 @@ -226,7 +227,7 @@ const useDownloadFileMutation = () => { }, onError: (error) => { if ((error as AxiosError)?.response?.status === 400) { - toast.error('댓글 작성이 필요합니다.'); + toast.error(BOARD.error.requiredComment); } }, }); diff --git a/src/api/seminarApi.ts b/src/api/seminarApi.ts index d5ba3ac12..5523d10d1 100644 --- a/src/api/seminarApi.ts +++ b/src/api/seminarApi.ts @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from 'react-query'; import axios from 'axios'; import { DateTime } from 'luxon'; +import { SEMINAR } from '@constants/apiResponseMessage'; import { useApiError } from '@hooks/useGetApiError'; import { AttendSeminarListInfo, SeminarStatus, SeminarInfo, SeminarCardInfo } from './dto'; @@ -143,7 +144,7 @@ const useAddSeminarMutation = ({ setHelperText }: { setHelperText: React.Dispatc const { handleError } = useApiError({ 409: { 40901: () => { - setHelperText('동일한 날짜의 세미나는 생성할 수 없습니다.'); + setHelperText(SEMINAR.error.duplicateSeminarDate); }, }, }); diff --git a/src/assets/notFound/404.svg b/src/assets/notFound/404.svg index ddc01c0f0..f9a4a6f67 100644 --- a/src/assets/notFound/404.svg +++ b/src/assets/notFound/404.svg @@ -1,19 +1,19 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/components/Layout/Header/Header.tsx b/src/components/Layout/Header/Header.tsx index 38674027e..ea4f77946 100644 --- a/src/components/Layout/Header/Header.tsx +++ b/src/components/Layout/Header/Header.tsx @@ -4,7 +4,7 @@ import { AppBar, IconButton, Toolbar, Typography } from '@mui/material'; import { VscAccount, VscGithubInverted, VscThreeBars } from 'react-icons/vsc'; import { useRecoilValue } from 'recoil'; import { ReactComponent as Logo } from '@assets/logo/logo_neon.svg'; -import { HEADER_HEIGHT } from '@constants/keeperTheme'; +import { KEEPER_HEIGHT, KEEPER_COLOR } from '@constants/keeperTheme'; import memberState from '@recoil/member.recoil'; import FilledButton from '@components/Button/FilledButton'; import AccountMenu from './Menu/AccountMenu'; @@ -33,7 +33,7 @@ const Header = ({ setMobileSidebarOpen }: HeaderProps) => { theme.zIndex.drawer + 1, height: HEADER_HEIGHT }} + sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, height: KEEPER_HEIGHT.header }} >
    @@ -57,10 +57,10 @@ const Header = ({ setMobileSidebarOpen }: HeaderProps) => { W - + - +
    diff --git a/src/components/Layout/Sidebar/Sidebar.tsx b/src/components/Layout/Sidebar/Sidebar.tsx index b3e01826f..4231b0ef6 100644 --- a/src/components/Layout/Sidebar/Sidebar.tsx +++ b/src/components/Layout/Sidebar/Sidebar.tsx @@ -3,7 +3,8 @@ import { Button, Drawer, Toolbar, useMediaQuery, useTheme } from '@mui/material' import { VscBug } from 'react-icons/vsc'; import { Role } from '@api/dto'; import CATEGORIES from '@constants/category'; -import { KEEPER_COLOR, SIDEBAR_WIDTH } from '@constants/keeperTheme'; +import { KEEPER_COLOR, KEEPER_WIDTH } from '@constants/keeperTheme'; +import { MEMBER_ROLE } from '@constants/member'; import useCheckAuth from '@hooks/useCheckAuth'; import CategoryNav from '@components/Navigation/CategoryNav'; @@ -13,7 +14,13 @@ interface SidebarProps { } const Sidebar = ({ mobileSidebarOpen, setMobileSidebarOpen }: SidebarProps) => { - const executiveRoles: Role[] = ['ROLE_회장', 'ROLE_부회장', 'ROLE_서기', 'ROLE_총무', 'ROLE_사서']; + const executiveRoles: Role[] = [ + MEMBER_ROLE.회장, + MEMBER_ROLE.부회장, + MEMBER_ROLE.서기, + MEMBER_ROLE.총무, + MEMBER_ROLE.사서, + ]; const { checkIncludeOneOfAuths } = useCheckAuth(); const theme = useTheme(); @@ -26,7 +33,7 @@ const Sidebar = ({ mobileSidebarOpen, setMobileSidebarOpen }: SidebarProps) => { open={mobileSidebarOpen} onClose={() => setMobileSidebarOpen(false)} sx={{ - [`& .MuiDrawer-paper`]: { width: SIDEBAR_WIDTH, bgcolor: KEEPER_COLOR.mainBlack }, + [`& .MuiDrawer-paper`]: { width: KEEPER_WIDTH.sidebar, bgcolor: KEEPER_COLOR.mainBlack }, }} > diff --git a/src/constants/apiResponseMessage.ts b/src/constants/apiResponseMessage.ts new file mode 100644 index 000000000..9453302c4 --- /dev/null +++ b/src/constants/apiResponseMessage.ts @@ -0,0 +1,57 @@ +export const COMMON = {} as const; + +export const PASSWORD = { + success: { + changed: '비밀번호가 변경되었습니다.', + }, + error: { + mismatch: '현재 비밀번호가 일치하지 않습니다.', + }, +} as const; + +export const BOARD = { + success: {}, + error: { + readCondition: '게시글 열람 조건을 충족하지 않습니다.', + mismatchPassword: '게시글의 비밀번호가 일치하지 않습니다.', + requiredComment: '댓글 작성이 필요합니다.', + }, +} as const; + +export const SEMINAR = { + success: {}, + error: { + duplicateSeminarDate: '동일한 날짜의 세미나는 생성할 수 없습니다.', + }, +} as const; + +export const EMAIL = { + success: { + changed: '이메일 변경 성공하였습니다.', + }, + error: { + existing: '이미 존재하는 이메일입니다.', + }, +} as const; + +export const MEMBER_CARD = { + success: {}, + error: { + noSubmissionsLeft: '남은 제출 횟수가 없습니다.', + mismatchWithCount: (min: number) => `출석코드가 틀렸습니다. (남은 제출횟수 ${min}회)` as const, + }, +} as const; + +export const LOGIN_ID = { + success: {}, + error: { + existing: '이미 존재하는 아이디입니다.', + }, +}; + +export const STUDENT_ID = { + success: {}, + error: { + existing: '이미 존재하는 학번입니다.', + }, +}; diff --git a/src/constants/badge.ts b/src/constants/badge.ts index 448e6ab9d..d75cb6ffc 100644 --- a/src/constants/badge.ts +++ b/src/constants/badge.ts @@ -9,18 +9,19 @@ import librarianBadge from '@assets/dutyManage/badge_8_librarian.gif'; import graduateBadge from '@assets/profileBadge/profile_badge_state_graduate.gif'; import regularBadge from '@assets/profileBadge/profile_badge_state_regular.gif'; import sleepBadge from '@assets/profileBadge/profile_badge_state_sleep.gif'; +import { MEMBER_ROLE } from './member'; const roles = [ - { name: 'ROLE_회장', img: chairmanBadge }, - { name: 'ROLE_부회장', img: viceChairmanBadge }, - { name: 'ROLE_서기', img: clerkBadge }, - { name: 'ROLE_총무', img: administratorBadge }, - { name: 'ROLE_사서', img: librarianBadge }, - { name: 'ROLE_학술부장', img: studyManagerBadge }, - { name: 'ROLE_대외부장', img: externalManagerBadge }, - { name: 'ROLE_FRONT_전산관리자', img: ITManagerBadge }, - { name: 'ROLE_BACK_전산관리자', img: ITManagerBadge }, - { name: 'ROLE_INFRA_전산관리자', img: ITManagerBadge }, + { name: MEMBER_ROLE.회장, img: chairmanBadge }, + { name: MEMBER_ROLE.부회장, img: viceChairmanBadge }, + { name: MEMBER_ROLE.서기, img: clerkBadge }, + { name: MEMBER_ROLE.총무, img: administratorBadge }, + { name: MEMBER_ROLE.사서, img: librarianBadge }, + { name: MEMBER_ROLE.학술부장, img: studyManagerBadge }, + { name: MEMBER_ROLE.대외부장, img: externalManagerBadge }, + { name: MEMBER_ROLE.FRONT_전산관리자, img: ITManagerBadge }, + { name: MEMBER_ROLE.BACK_전산관리자, img: ITManagerBadge }, + { name: MEMBER_ROLE.INFRA_전산관리자, img: ITManagerBadge }, ]; const types: { [key: string]: string } = { 정회원: regularBadge, 졸업: graduateBadge, 휴면회원: sleepBadge }; diff --git a/src/constants/category.ts b/src/constants/category.ts index d637759a0..6e9a0e857 100644 --- a/src/constants/category.ts +++ b/src/constants/category.ts @@ -1,4 +1,5 @@ import { Role } from '@api/dto'; +import { MEMBER_ROLE } from '@constants/member'; export interface CategoryMenu { id: number; @@ -152,19 +153,19 @@ const CATEGORIES: Category[] = [ id: 605, name: '문제관리', path: 'admin/challengeManage', - roles: ['ROLE_회장', 'ROLE_출제자'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.출제자], }, { id: 606, name: '제출로그', path: 'admin/submissions', - roles: ['ROLE_회장', 'ROLE_출제자'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.출제자], }, { id: 607, name: '대회운영', path: 'admin/operation', - roles: ['ROLE_회장'], + roles: [MEMBER_ROLE.회장], }, ], }, */ @@ -177,37 +178,37 @@ const CATEGORIES: Category[] = [ id: 701, name: '직책관리', path: 'dutyManage', - roles: ['ROLE_회장', 'ROLE_부회장'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.부회장], }, /* { id: 702, name: '선거관리', path: 'electionManage', - roles: ['ROLE_회장'], + roles: [MEMBER_ROLE.회장], }, */ { id: 703, name: '도서관리', path: 'libraryManage', - roles: ['ROLE_회장', 'ROLE_부회장', 'ROLE_사서'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.부회장, MEMBER_ROLE.사서], }, { id: 704, name: '세미나관리', path: 'seminarManage', - roles: ['ROLE_회장', 'ROLE_부회장', 'ROLE_서기'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.부회장, MEMBER_ROLE.서기], }, { id: 705, name: '활동인원관리', path: 'activeMemberManage', - roles: ['ROLE_회장', 'ROLE_부회장', 'ROLE_서기'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.부회장, MEMBER_ROLE.서기], }, { id: 706, name: '상벌점관리', path: 'meritManage', - roles: ['ROLE_회장', 'ROLE_부회장', 'ROLE_서기'], + roles: [MEMBER_ROLE.회장, MEMBER_ROLE.부회장, MEMBER_ROLE.서기], }, ], }, diff --git a/src/constants/errorMsg.ts b/src/constants/errorMsg.ts deleted file mode 100644 index 6a4c7c388..000000000 --- a/src/constants/errorMsg.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const REQUIRE_ERROR_MSG = '필수 정보입니다.'; -export const NUMBER_ERROR_MSG = '숫자만 입력 가능합니다.'; diff --git a/src/constants/helperText.ts b/src/constants/helperText.ts new file mode 100644 index 000000000..21831a12e --- /dev/null +++ b/src/constants/helperText.ts @@ -0,0 +1,62 @@ +export const COMMON = { + success: {}, + error: { + required: '필수 정보입니다.', + onlyNumber: '숫자만 입력 가능합니다.', + onlyHttps: 'https:// 로 시작해야 합니다.', + minLength: (min: number) => `${min}글자 이상 입력해주세요.` as const, + maxLength: (max: number) => `최대 글자수 ${max}글자를 초과했습니다.` as const, + }, +} as const; + +export const EMAIL_MSG = { + success: {}, + error: { + formatError: '이메일 형식을 확인해주세요.', + }, +} as const; + +export const CONFIRM_PASSWORD_MSG = { + success: { + match: '비밀번호가 일치합니다.', + }, + error: { + mismatch: '비밀번호가 일치하지 않습니다.', + formatError: '8~20자 영문과 숫자를 사용하세요.', + }, +} as const; + +export const NAME_MSG = { + success: {}, + error: { + formatError: '1~20자 한글, 영어만 가능합니다.', + }, +} as const; + +export const SEND_POINT_MSG = { + success: {}, + error: { + overMaxValue: '보유 포인트보다 많은 포인트를 보낼 수 없습니다.', + }, +} as const; + +export const LOGIN_ID_MSG = { + success: {}, + error: { + formatError: '4~12자 영어, 숫자, _ 만 가능합니다.', + }, +} as const; + +export const STUDY_MSG = { + success: {}, + error: { + onlyGitLink: '깃허브 링크만 입력이 가능합니다.', + }, +} as const; + +export const BOARD_MSG = { + success: {}, + error: { + requiredPassword: '작성자가 아니면 비밀번호가 필요합니다.', + }, +}; diff --git a/src/constants/keeperTheme.ts b/src/constants/keeperTheme.ts index 4a3cff33d..c64740955 100644 --- a/src/constants/keeperTheme.ts +++ b/src/constants/keeperTheme.ts @@ -5,7 +5,14 @@ export const KEEPER_COLOR = { pointBlue: '#4CEEF9', subGray: '#575E69', subRed: '#EF4444', -}; + subOrange: '#FFA500', +} as const; -export const SIDEBAR_WIDTH = 240; -export const HEADER_HEIGHT = { xs: 56, sm: 66 }; +export const KEEPER_WIDTH = { + sidebar: 240, + container: 1080, +} as const; + +export const KEEPER_HEIGHT = { + header: { xs: 56, sm: 66 }, +} as const; diff --git a/src/constants/member.ts b/src/constants/member.ts new file mode 100644 index 000000000..7e42e4442 --- /dev/null +++ b/src/constants/member.ts @@ -0,0 +1,17 @@ +export const MEMBER_ROLE_PREFIX = 'ROLE_'; + +export const MEMBER_ROLE = { + 회장: `${MEMBER_ROLE_PREFIX}회장`, + 부회장: `${MEMBER_ROLE_PREFIX}부회장`, + 서기: `${MEMBER_ROLE_PREFIX}서기`, + 총무: `${MEMBER_ROLE_PREFIX}총무`, + 사서: `${MEMBER_ROLE_PREFIX}사서`, + 학술부장: `${MEMBER_ROLE_PREFIX}학술부장`, + 대외부장: `${MEMBER_ROLE_PREFIX}대외부장`, + 전산관리자: `${MEMBER_ROLE_PREFIX}전산관리자`, + FRONT_전산관리자: `${MEMBER_ROLE_PREFIX}FRONT_전산관리자`, + BACK_전산관리자: `${MEMBER_ROLE_PREFIX}BACK_전산관리자`, + INFRA_전산관리자: `${MEMBER_ROLE_PREFIX}INFRA_전산관리자`, + 회원: `${MEMBER_ROLE_PREFIX}회원`, + 출제자: `${MEMBER_ROLE_PREFIX}출제자`, +} as const; diff --git a/src/constants/muiTheme.ts b/src/constants/muiTheme.ts index d95977ad1..8a17f9529 100644 --- a/src/constants/muiTheme.ts +++ b/src/constants/muiTheme.ts @@ -5,11 +5,11 @@ const muiTheme = createTheme({ palette: { mode: 'dark', primary: { - main: '#4CEEF9', + main: KEEPER_COLOR.pointBlue, }, secondary: { - main: '#26262C', - contrastText: '#4CEEF9', + main: KEEPER_COLOR.subBlack, + contrastText: KEEPER_COLOR.pointBlue, }, }, typography: { diff --git a/src/mocks/DutyManageApi.ts b/src/mocks/DutyManageApi.ts index 9701a291d..f7cb40f6c 100644 --- a/src/mocks/DutyManageApi.ts +++ b/src/mocks/DutyManageApi.ts @@ -6,6 +6,7 @@ import ITManagerBadge from '@assets/dutyManage/badge_5_it_manager.gif'; import clerkBadge from '@assets/dutyManage/badge_6_clerk.gif'; import administratorBadge from '@assets/dutyManage/badge_7_administrator.gif'; import librarianBadge from '@assets/dutyManage/badge_8_librarian.gif'; +import { MEMBER_ROLE } from '@constants/member'; interface RoleDutyList { jobName: string; @@ -82,14 +83,16 @@ const roleDutyListInfo: RoleDutyList[] = [ ]; const roles = [ - { name: 'ROLE_회장', img: chairmanBadge }, - { name: 'ROLE_부회장', img: viceChairmanBadge }, - { name: 'ROLE_대외부장', img: externalManagerBadge }, - { name: 'ROLE_학술부장', img: studyManagerBadge }, - { name: 'ROLE_전산관리자', img: ITManagerBadge }, - { name: 'ROLE_서기', img: clerkBadge }, - { name: 'ROLE_총무', img: administratorBadge }, - { name: 'ROLE_사서', img: librarianBadge }, + { name: MEMBER_ROLE.회장, img: chairmanBadge }, + { name: MEMBER_ROLE.부회장, img: viceChairmanBadge }, + { name: MEMBER_ROLE.대외부장, img: externalManagerBadge }, + { name: MEMBER_ROLE.학술부장, img: studyManagerBadge }, + { name: MEMBER_ROLE.FRONT_전산관리자, img: ITManagerBadge }, + { name: MEMBER_ROLE.BACK_전산관리자, img: ITManagerBadge }, + { name: MEMBER_ROLE.INFRA_전산관리자, img: ITManagerBadge }, + { name: MEMBER_ROLE.서기, img: clerkBadge }, + { name: MEMBER_ROLE.총무, img: administratorBadge }, + { name: MEMBER_ROLE.사서, img: librarianBadge }, ]; export type JobInfoType = { diff --git a/src/pages/Library/Library.tsx b/src/pages/Library/Library.tsx index cb340e4c0..9df2ea924 100644 --- a/src/pages/Library/Library.tsx +++ b/src/pages/Library/Library.tsx @@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography'; import { BookListSearch } from '@api/dto'; import { useGetExecutiveInfoQuery } from '@api/dutyManageApi'; import { useGetBookListQuery, useRequestBorrowBookMutation, useGetBookBorrowsQuery } from '@api/libraryApi'; +import { MEMBER_ROLE } from '@constants/member'; import usePagination from '@hooks/usePagination'; import StandardTablePagination from '@components/Pagination/StandardTablePagination'; import PageTitle from '@components/Typography/PageTitle'; @@ -28,7 +29,7 @@ const Library = () => { const { data: executiveInfos } = useGetExecutiveInfoQuery(); const { mutate: RequestBorrowBook } = useRequestBorrowBookMutation(); - const librarian = executiveInfos?.find((role) => role.jobName === 'ROLE_사서')?.realName || ''; + const librarian = executiveInfos?.find((role) => role.jobName === MEMBER_ROLE.사서)?.realName || ''; const handleRequestBook = (bookId: number) => { RequestBorrowBook(bookId, { diff --git a/src/pages/NotFound/SvgComponent/Svg404Component.tsx b/src/pages/NotFound/SvgComponent/Svg404Component.tsx index d2cbbcfd6..f1dc7e5f9 100644 --- a/src/pages/NotFound/SvgComponent/Svg404Component.tsx +++ b/src/pages/NotFound/SvgComponent/Svg404Component.tsx @@ -1,10 +1,13 @@ import React from 'react'; +import { KEEPER_COLOR } from '@constants/keeperTheme'; interface Svg404Component { point: { top: number; left: number }; handleKeyClick: () => void; } +const PUPIL_COLOR = '#131316'; + /* NOTE src/assets/notFound/404.svg에서 추가적인 이벤트 처리한 컴포넌트 */ const Svg404Component = ({ point, handleKeyClick }: Svg404Component) => { return ( @@ -26,46 +29,46 @@ const Svg404Component = ({ point, handleKeyClick }: Svg404Component) => { transform="rotate(88.8057 178.791 64)" fill="url(#pattern0)" /> - - - + + + window.innerWidth * (3 / 4) ? 157 + point.left / (window.innerWidth / 4) : 159} cy={41 + point.top / (window.innerHeight / 4)} rx="3.5" ry="4" - fill="#131316" + fill={PUPIL_COLOR} /> - + window.innerWidth * (3 / 4) ? 183 + point.left / (window.innerWidth / 4) : 185} cy={41 + point.top / (window.innerHeight / 4)} rx="3.5" ry="4" - fill="#131316" + fill={PUPIL_COLOR} /> - - + + - - - + + + diff --git a/src/pages/Profile/Modal/EditAccountModal.tsx b/src/pages/Profile/Modal/EditAccountModal.tsx index 589e83c07..2cde6ec3b 100644 --- a/src/pages/Profile/Modal/EditAccountModal.tsx +++ b/src/pages/Profile/Modal/EditAccountModal.tsx @@ -14,7 +14,8 @@ import { useWithdrawalMutation, } from '@api/memberApi'; import { useCheckEmailDuplicationQuery } from '@api/signUpApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { EMAIL } from '@constants/apiResponseMessage'; +import { COMMON, EMAIL_MSG, CONFIRM_PASSWORD_MSG } from '@constants/helperText'; import memberState from '@recoil/member.recoil'; import { emailRegex } from '@utils/validateEmail'; import FilledButton from '@components/Button/FilledButton'; @@ -57,7 +58,7 @@ const EditEmailSection = () => { { email, auth, password }, { onSuccess: () => { - toast.success('이메일 변경 성공하였습니다.'); + toast.success(EMAIL.success.changed); setIsEmailSent(false); reset(); }, @@ -69,7 +70,7 @@ const EditEmailSection = () => { if (!checkEmailDuplicationSuccess) return; if (isEmailDuplicate.duplicate === true) { - setError('email', { message: '이미 존재하는 이메일입니다.' }); + setError('email', { message: EMAIL.error.existing }); setIsEmailSent(false); return; } @@ -97,7 +98,7 @@ const EditEmailSection = () => { defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, }} render={({ field, fieldState: { error } }) => { return ( @@ -117,10 +118,10 @@ const EditEmailSection = () => { defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, pattern: { value: emailRegex, - message: '이메일 주소를 다시 확인해주세요.', + message: EMAIL_MSG.error.formatError, }, }} render={({ field, fieldState: { error, isDirty } }) => { @@ -144,7 +145,7 @@ const EditEmailSection = () => { defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, }} render={({ field, fieldState: { error } }) => { return ( @@ -206,7 +207,7 @@ const EditPasswordSection = () => { defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, }} render={({ field, fieldState: { error } }) => { return ( @@ -226,14 +227,14 @@ const EditPasswordSection = () => { defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, minLength: { value: 8, - message: '8글자 이상 입력해주세요.', + message: COMMON.error.minLength(8), }, pattern: { value: /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,20}$/, - message: '8~20자 영문과 숫자를 사용하세요.', + message: CONFIRM_PASSWORD_MSG.error.formatError, }, }} render={({ field, fieldState: { error } }) => { @@ -254,11 +255,11 @@ const EditPasswordSection = () => { defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, validate: { confirmMatchPassward: (value) => { - if (getValues('newPassword') !== value) return '비밀번호가 일치하지 않습니다.'; - setPasswordConfirmSuccessMsg('비밀번호가 일치합니다.'); + if (getValues('newPassword') !== value) return CONFIRM_PASSWORD_MSG.error.mismatch; + setPasswordConfirmSuccessMsg(CONFIRM_PASSWORD_MSG.success.match); return undefined; }, }, @@ -348,7 +349,7 @@ const EditAccountModal = ({ open, onClose }: EditAccountModalProps) => { defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, }} render={({ field, fieldState: { error } }) => { return ( diff --git a/src/pages/Profile/Modal/EditProfileModal.tsx b/src/pages/Profile/Modal/EditProfileModal.tsx index 6ba5a6759..e42a95819 100644 --- a/src/pages/Profile/Modal/EditProfileModal.tsx +++ b/src/pages/Profile/Modal/EditProfileModal.tsx @@ -3,7 +3,7 @@ import { Controller, FieldValues, SubmitHandler, useForm } from 'react-hook-form import { Stack } from '@mui/material'; import { ProfileInfo } from '@api/dto'; import { useEditProfileMutation, useEditProfileThumbnailMutation } from '@api/memberApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON, NAME_MSG } from '@constants/helperText'; import StandardDatePicker from '@components/DatePicker/StandardDatePicker'; import StandardInput from '@components/Input/StandardInput'; import ActionModal from '@components/Modal/ActionModal'; @@ -65,14 +65,14 @@ const EditProfileModal = ({ profileInfo, open, onClose }: EditProfileModalProps) defaultValue={profileInfo.realName} control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: NAME_MAX_LENGTH, - message: `이름은 최대 ${NAME_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(NAME_MAX_LENGTH), }, pattern: { value: /^[가-힣a-zA-Z]{1,20}$/, - message: '1~20자 한글, 영어만 가능합니다.', + message: NAME_MSG.error.formatError, }, }} render={({ field, fieldState: { error } }) => { diff --git a/src/pages/Profile/Modal/SendPointModal.tsx b/src/pages/Profile/Modal/SendPointModal.tsx index aa5e9c267..50df5edef 100644 --- a/src/pages/Profile/Modal/SendPointModal.tsx +++ b/src/pages/Profile/Modal/SendPointModal.tsx @@ -4,7 +4,7 @@ import { InputLabel } from '@mui/material'; import { useRecoilValue } from 'recoil'; import { useGetProfileQuery } from '@api/memberApi'; import { useSendPointMutation } from '@api/pointApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON, SEND_POINT_MSG } from '@constants/helperText'; import memberState from '@recoil/member.recoil'; import StandardInput from '@components/Input/StandardInput'; import ActionModal from '@components/Modal/ActionModal'; @@ -69,14 +69,14 @@ const SendPointModal = ({ open, onClose, sendTo }: SendPointModalProps) => { defaultValue={1} control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, max: { value: profileInfo.point, - message: '보유 포인트보다 많은 포인트를 보낼 수 없습니다.', + message: SEND_POINT_MSG.error.overMaxValue, }, pattern: { value: /^[0-9]+$/, - message: '숫자만 입력 가능합니다.', + message: COMMON.error.onlyNumber, }, }} render={({ field, fieldState: { error } }) => ( @@ -97,10 +97,10 @@ const SendPointModal = ({ open, onClose, sendTo }: SendPointModalProps) => { defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: SEND_POINT_MAX_LENGTH, - message: `최대 ${SEND_POINT_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(SEND_POINT_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => ( diff --git a/src/pages/Profile/Section/BadgeSection.tsx b/src/pages/Profile/Section/BadgeSection.tsx index c6e092184..e2d563904 100644 --- a/src/pages/Profile/Section/BadgeSection.tsx +++ b/src/pages/Profile/Section/BadgeSection.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Tooltip } from '@mui/material'; import { Role, RoleInfo } from '@api/dto'; import { roles, types } from '@constants/badge'; +import { MEMBER_ROLE_PREFIX, MEMBER_ROLE } from '@constants/member'; interface FollowListProps { memberType: string; @@ -13,7 +14,7 @@ const BadgeSection = ({ memberType, memberJobs }: FollowListProps) => { useEffect(() => { const updatedMemberJobInfo = memberJobs - .filter((job) => job !== 'ROLE_회원' && job !== 'ROLE_출제자') + .filter((job) => job !== MEMBER_ROLE.회원 && job !== MEMBER_ROLE.출제자) .map((job) => { const filteredRole = roles.find((role) => role.name === job); return { @@ -27,7 +28,7 @@ const BadgeSection = ({ memberType, memberJobs }: FollowListProps) => { return (
    {MemberJobInfo.map((job: RoleInfo) => ( - + ))} diff --git a/src/pages/Profile/Tab/BookTab/BookTab.tsx b/src/pages/Profile/Tab/BookTab/BookTab.tsx index 2fb69b095..6a7142331 100644 --- a/src/pages/Profile/Tab/BookTab/BookTab.tsx +++ b/src/pages/Profile/Tab/BookTab/BookTab.tsx @@ -7,6 +7,7 @@ import { useCancleReturnBookMutation, useCancleBorrowBookMutation, } from '@api/libraryApi'; +import { MEMBER_ROLE } from '@constants/member'; import BookCard from './Card/BookCard'; import BookGuide from './Guide/BookGuide'; @@ -20,7 +21,7 @@ const BookTab = () => { const { mutate: cancleReturnBookMutation } = useCancleReturnBookMutation(); const { mutate: cancleBorrowBookMutation } = useCancleBorrowBookMutation(); - const librarian = executiveInfos?.find((role) => role.jobName === 'ROLE_사서')?.realName || ''; + const librarian = executiveInfos?.find((role) => role.jobName === MEMBER_ROLE.사서)?.realName || ''; const borrowLength = borrowedBookListData?.content?.filter((bookInfo) => bookInfo.status === '대출대기').length; const returnLength = borrowedBookListData?.content?.filter((bookInfo) => bookInfo.status === '반납대기').length; diff --git a/src/pages/SignUp/Section/SignUpFirstInputSection.tsx b/src/pages/SignUp/Section/SignUpFirstInputSection.tsx index 26ae00350..ec1aeb68c 100644 --- a/src/pages/SignUp/Section/SignUpFirstInputSection.tsx +++ b/src/pages/SignUp/Section/SignUpFirstInputSection.tsx @@ -6,6 +6,8 @@ import { VscCheck } from 'react-icons/vsc'; import { useSetRecoilState } from 'recoil'; import { signUpKeys, useCheckLoginIdDuplicationQuery } from '@api/signUpApi'; +import { LOGIN_ID } from '@constants/apiResponseMessage'; +import { COMMON, LOGIN_ID_MSG, CONFIRM_PASSWORD_MSG } from '@constants/helperText'; import FilledButton from '@components/Button/FilledButton'; import OutlinedButton from '@components/Button/OutlinedButton'; import StandardInput from '@components/Input/StandardInput'; @@ -50,7 +52,7 @@ const SignUpFirstInputSection = ({ setCurrentStep }: SignUpFirstInputSectionProp if (!isLoginIdDuplicate) return; if (isLoginIdDuplicate.duplicate === true) { - setError('loginId', { message: '이미 존재하는 아이디입니다.' }); + setError('loginId', { message: LOGIN_ID.error.existing }); setCheckLoginIdDuplicateEnabled(false); } }, [isLoginIdDuplicate]); @@ -71,14 +73,14 @@ const SignUpFirstInputSection = ({ setCurrentStep }: SignUpFirstInputSectionProp defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, minLength: { value: 4, - message: '4글자 이상 입력해주세요.', + message: COMMON.error.minLength(4), }, pattern: { value: /^[a-zA-Z0-9_]{4,12}$/, - message: '4~12자 영어, 숫자, _ 만 가능합니다.', + message: LOGIN_ID_MSG.error.formatError, }, }} render={({ field, fieldState: { error, isDirty } }) => { @@ -107,14 +109,14 @@ const SignUpFirstInputSection = ({ setCurrentStep }: SignUpFirstInputSectionProp defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, minLength: { value: 8, - message: '8글자 이상 입력해주세요.', + message: COMMON.error.minLength(8), }, pattern: { value: /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,20}$/, - message: '8~20자 영문과 숫자를 사용하세요.', + message: CONFIRM_PASSWORD_MSG.error.formatError, }, }} render={({ field, fieldState: { error } }) => { @@ -135,11 +137,11 @@ const SignUpFirstInputSection = ({ setCurrentStep }: SignUpFirstInputSectionProp defaultValue="" control={control} rules={{ - required: '필수 정보입니다.', + required: COMMON.error.required, validate: { confirmMatchPassward: (value) => { - if (getValues('password') !== value) return '비밀번호가 일치하지 않습니다.'; - setPasswordConfirmSuccessMsg('비밀번호가 일치합니다.'); + if (getValues('password') !== value) return CONFIRM_PASSWORD_MSG.error.mismatch; + setPasswordConfirmSuccessMsg(CONFIRM_PASSWORD_MSG.success.match); return undefined; }, }, diff --git a/src/pages/SignUp/Section/SignUpSecondInputSection.tsx b/src/pages/SignUp/Section/SignUpSecondInputSection.tsx index 8e454ffc2..b9b08f42f 100644 --- a/src/pages/SignUp/Section/SignUpSecondInputSection.tsx +++ b/src/pages/SignUp/Section/SignUpSecondInputSection.tsx @@ -5,7 +5,8 @@ import { Stack } from '@mui/material'; import { VscCheck } from 'react-icons/vsc'; import { useSetRecoilState } from 'recoil'; import { signUpKeys, useCheckStudentIdDuplicationQuery } from '@api/signUpApi'; -import { NUMBER_ERROR_MSG, REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { STUDENT_ID } from '@constants/apiResponseMessage'; +import { COMMON, NAME_MSG } from '@constants/helperText'; import FilledButton from '@components/Button/FilledButton'; import OutlinedButton from '@components/Button/OutlinedButton'; import StandardDatePicker from '@components/DatePicker/StandardDatePicker'; @@ -57,7 +58,7 @@ const SignUpSecondInputSection = ({ setCurrentStep }: SignUpFirstInputSectionPro if (!isStudentIdDuplicate) return; if (isStudentIdDuplicate.duplicate === true) { - setError('studentId', { message: '이미 존재하는 학번입니다.' }); + setError('studentId', { message: STUDENT_ID.error.existing }); setCheckStudentIdDuplicateEnabled(false); } }, [isStudentIdDuplicate]); @@ -78,14 +79,14 @@ const SignUpSecondInputSection = ({ setCurrentStep }: SignUpFirstInputSectionPro defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: NAME_MAX_LENGTH, - message: `이름은 최대 ${NAME_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(NAME_MAX_LENGTH), }, pattern: { value: /^[가-힣a-zA-Z]{1,20}$/, - message: '1~20자 한글, 영어만 가능합니다.', + message: NAME_MSG.error.formatError, }, }} render={({ field, fieldState: { error } }) => { @@ -99,10 +100,10 @@ const SignUpSecondInputSection = ({ setCurrentStep }: SignUpFirstInputSectionPro defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, pattern: { value: /^[0-9]+$/, - message: NUMBER_ERROR_MSG, + message: COMMON.error.onlyNumber, }, }} render={({ field, fieldState: { error, isDirty } }) => { diff --git a/src/pages/SignUp/Section/SignUpThirdInputSection.tsx b/src/pages/SignUp/Section/SignUpThirdInputSection.tsx index 0d32074e4..48c6e2041 100644 --- a/src/pages/SignUp/Section/SignUpThirdInputSection.tsx +++ b/src/pages/SignUp/Section/SignUpThirdInputSection.tsx @@ -6,7 +6,8 @@ import { Stack, Typography } from '@mui/material'; import { DateTime } from 'luxon'; import { useRecoilValue } from 'recoil'; import { useCheckEmailDuplicationQuery, useEmailAuthMutation, useSignUpMutation } from '@api/signUpApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { EMAIL } from '@constants/apiResponseMessage'; +import { COMMON, EMAIL_MSG } from '@constants/helperText'; import { emailRegex } from '@utils/validateEmail'; import OutlinedButton from '@components/Button/OutlinedButton'; import EmailAuthInput from '@components/Input/EmailAuthInput'; @@ -73,7 +74,7 @@ const SignUpThirdInputSection = () => { if (!checkEmailDuplicationSuccess) return; if (isEmailDuplicate.duplicate === true) { - setError('email', { message: '이미 존재하는 이메일입니다.' }); + setError('email', { message: EMAIL.error.existing }); setIsEmailSent(false); return; } @@ -92,10 +93,10 @@ const SignUpThirdInputSection = () => { defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, pattern: { value: emailRegex, - message: '이메일 주소를 다시 확인해주세요.', + message: EMAIL_MSG.error.formatError, }, }} render={({ field, fieldState: { error, isDirty } }) => { @@ -119,7 +120,7 @@ const SignUpThirdInputSection = () => { defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, }} render={({ field, fieldState: { error } }) => { return ( diff --git a/src/pages/SignUp/SignUp.tsx b/src/pages/SignUp/SignUp.tsx index ad36540ec..108688616 100644 --- a/src/pages/SignUp/SignUp.tsx +++ b/src/pages/SignUp/SignUp.tsx @@ -7,15 +7,15 @@ import SignUpFirstInputSection from './Section/SignUpFirstInputSection'; import SignUpSecondInputSection from './Section/SignUpSecondInputSection'; import SignUpThirdInputSection from './Section/SignUpThirdInputSection'; -const SignUp = () => { - const TOTAL_STEPS = 3; +const TOTAL_STEPS = 3; +const STEP_INFO_MESSAGE = { + 1: '로그인에 사용할\n아이디와 비밀번호를 등록해 주세요.', + 2: '프로필 정보를 등록해 주세요.', + 3: '이메일 주소를 입력해주세요.\n입력한 이메일 주소로 인증 코드가 발송됩니다.', +}; +const SignUp = () => { const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1); - const stepInfoMsg = { - 1: '로그인에 사용할\n아이디와 비밀번호를 등록해 주세요.', - 2: '프로필 정보를 등록해 주세요.', - 3: '이메일 주소를 입력해주세요.\n입력한 이메일 주소로 인증 코드가 발송됩니다.', - }; const stepInputSection = { 1: , @@ -29,7 +29,7 @@ const SignUp = () => { - {stepInfoMsg[currentStep]} + {STEP_INFO_MESSAGE[currentStep]} {stepInputSection[currentStep]} diff --git a/src/pages/Study/Modal/StudyModal.tsx b/src/pages/Study/Modal/StudyModal.tsx index 5ed2f9350..299dd5afb 100644 --- a/src/pages/Study/Modal/StudyModal.tsx +++ b/src/pages/Study/Modal/StudyModal.tsx @@ -14,7 +14,7 @@ import { useEditStudyThumbnailMutation, useGetStudyQuery, } from '@api/studyApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON, STUDY_MSG } from '@constants/helperText'; import memberState from '@recoil/member.recoil'; import AutoComplete, { MultiAutoCompleteValue } from '@components/Input/AutoComplete'; import StandardInput from '@components/Input/StandardInput'; @@ -168,10 +168,10 @@ const StudyModal = ({ open, setOpen, selectedStudyInfo, setSelectedStudyInfo, cu defaultValue={selectedStudyInfo?.title ?? ''} control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: STUDY_TITLE_MAX_LENGTH, - message: `최대 ${STUDY_TITLE_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(STUDY_TITLE_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => { @@ -194,10 +194,10 @@ const StudyModal = ({ open, setOpen, selectedStudyInfo, setSelectedStudyInfo, cu defaultValue={studyDetail?.information ?? ''} control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: STUDY_CONTENT_MAX_LENGTH, - message: `최대 ${STUDY_CONTENT_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(STUDY_CONTENT_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => { @@ -265,7 +265,7 @@ const StudyModal = ({ open, setOpen, selectedStudyInfo, setSelectedStudyInfo, cu rules={{ pattern: { value: /^(https:\/\/github.com)/, - message: `깃헙 링크만 입력이 가능합니다.`, + message: STUDY_MSG.error.onlyGitLink, }, }} render={({ field, fieldState: { error } }) => { @@ -296,7 +296,7 @@ const StudyModal = ({ open, setOpen, selectedStudyInfo, setSelectedStudyInfo, cu rules={{ pattern: { value: /^(https:\/\/)/, - message: `https:// 로 시작해야 합니다.`, + message: COMMON.error.onlyHttps, }, }} render={({ field, fieldState: { error } }) => { @@ -347,7 +347,7 @@ const StudyModal = ({ open, setOpen, selectedStudyInfo, setSelectedStudyInfo, cu rules={{ pattern: { value: /^(https:\/\/)/, - message: `https:// 로 시작해야 합니다.`, + message: COMMON.error.onlyHttps, }, }} render={({ field, fieldState: { error } }) => { diff --git a/src/pages/admin/DutyManage/Button/DutyProfileButton.tsx b/src/pages/admin/DutyManage/Button/DutyProfileButton.tsx index 443a25394..76456672a 100644 --- a/src/pages/admin/DutyManage/Button/DutyProfileButton.tsx +++ b/src/pages/admin/DutyManage/Button/DutyProfileButton.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Button, Typography } from '@mui/material'; import { useGetExecutiveInfoQuery } from '@api/dutyManageApi'; +import { MEMBER_ROLE } from '@constants/member'; import muiTheme from '@constants/muiTheme'; import { convertJobName } from '@mocks/DutyManageApi'; @@ -21,7 +22,7 @@ const DutyProfileButton = ({ jobName, badgeImage, setTooltipOpen, toggleModalOpe toggleModalOpen(); }; - if (jobName === 'ROLE_전산관리자') { + if (jobName === MEMBER_ROLE.전산관리자) { return (
    diff --git a/src/pages/admin/DutyManage/DutyManage.tsx b/src/pages/admin/DutyManage/DutyManage.tsx index 01e24d5da..db45acf96 100644 --- a/src/pages/admin/DutyManage/DutyManage.tsx +++ b/src/pages/admin/DutyManage/DutyManage.tsx @@ -1,21 +1,21 @@ import React from 'react'; +import { MEMBER_ROLE } from '@constants/member'; import PageTitle from '@components/Typography/PageTitle'; - import DutyProfileTooltip from './Tooltip/DutyProfileTooltip'; const jobNameArray = [ - { key: 1, jobName: 'ROLE_학술부장' }, - { key: 2, jobName: 'ROLE_대외부장' }, - { key: 3, jobName: 'ROLE_전산관리자' }, - { key: 4, jobName: 'ROLE_서기' }, - { key: 5, jobName: 'ROLE_사서' }, - { key: 6, jobName: 'ROLE_총무' }, + { key: 1, jobName: MEMBER_ROLE.학술부장 }, + { key: 2, jobName: MEMBER_ROLE.대외부장 }, + { key: 3, jobName: MEMBER_ROLE.전산관리자 }, + { key: 4, jobName: MEMBER_ROLE.서기 }, + { key: 5, jobName: MEMBER_ROLE.사서 }, + { key: 6, jobName: MEMBER_ROLE.총무 }, ]; const ITjobNameArray = [ - { key: 1, jobName: 'ROLE_FRONT_전산관리자' }, - { key: 2, jobName: 'ROLE_BACK_전산관리자' }, - { key: 3, jobName: 'ROLE_INFRA_전산관리자' }, + { key: 1, jobName: MEMBER_ROLE.FRONT_전산관리자 }, + { key: 2, jobName: MEMBER_ROLE.BACK_전산관리자 }, + { key: 3, jobName: MEMBER_ROLE.INFRA_전산관리자 }, ]; const MiddleBar = () => { @@ -66,7 +66,7 @@ const ViceChairman = () => {
    - +
    @@ -78,7 +78,7 @@ const DutyManage = () => {
    직책관리
    - + diff --git a/src/pages/admin/DutyManage/Tooltip/DutyProfileTooltip.tsx b/src/pages/admin/DutyManage/Tooltip/DutyProfileTooltip.tsx index 0887743aa..de8b48605 100644 --- a/src/pages/admin/DutyManage/Tooltip/DutyProfileTooltip.tsx +++ b/src/pages/admin/DutyManage/Tooltip/DutyProfileTooltip.tsx @@ -3,6 +3,7 @@ import { Typography } from '@mui/material'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import { VscSearch } from 'react-icons/vsc'; +import { MEMBER_ROLE } from '@constants/member'; import muiTheme from '@constants/muiTheme'; import { roleDutyListInfo, roles } from '@mocks/DutyManageApi'; @@ -50,13 +51,8 @@ const DutyProfileTooltip = ({ jobName }: DutyProfileTooltipProps) => { const [tooltipOpen, setTooltipOpen] = useState(false); const [modalOpen, toggleModalOpen] = useReducer((prev) => !prev, false); - let badgeImage; - if (jobName.search('전산관리자') !== -1) { - badgeImage = roles.find((role) => role.name === 'ROLE_전산관리자')?.img; - } else { - badgeImage = roles.find((role) => role.name === jobName)?.img; - } - + const badgeImage = roles.find((role) => role.name === jobName)?.img; + // NOTE jobName으로는 "ROLE_전산관리자" 내려오지만, roles에는 존재하지 않습니다! 세부적인 전산관리자(프론트, 백, 인프라)만 존재합니다 return (
    { setTooltipOpen={setTooltipOpen} toggleModalOpen={toggleModalOpen} /> - {jobName !== 'ROLE_전산관리자' ? ( + {jobName !== MEMBER_ROLE.전산관리자 && ( <> { badgeImage={badgeImage} /> - ) : null} + )}
    ); }; diff --git a/src/pages/admin/LibraryManage/Modal/UploadBookModal.tsx b/src/pages/admin/LibraryManage/Modal/UploadBookModal.tsx index da29a157d..4dbd95232 100644 --- a/src/pages/admin/LibraryManage/Modal/UploadBookModal.tsx +++ b/src/pages/admin/LibraryManage/Modal/UploadBookModal.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Typography, Tooltip } from '@mui/material'; import { ManageBookInfo } from '@api/dto'; import { useAddBookMutation, useEditBookInfoMutation, useEditBookThumbnailMutation } from '@api/libraryManageApi'; +import { COMMON } from '@constants/helperText'; import StandardInput from '@components/Input/StandardInput'; import ActionModal from '@components/Modal/ActionModal'; import ImageUploader from '@components/Uploader/ImageUploader'; @@ -40,19 +41,19 @@ const UploadBookModal = ({ open, onClose, bookDetail }: SelectorProps) => { const authorTrim = author.trim(); if (titleTrim === '') { - setTitleHelperText('도서명을 입력해주세요'); + setTitleHelperText(COMMON.error.required); setIsInvalidTitle(true); } if (authorTrim === '') { - setAuthorHelperText('저자명을 입력해주세요'); + setAuthorHelperText(COMMON.error.required); setIsInvalidAuthor(true); } - if (titleTrim.length > 30) { - setTitleHelperText('도서명은 200자 이내여야 합니다.'); + if (titleTrim.length > 200) { + setTitleHelperText(COMMON.error.maxLength(200)); setIsInvalidTitle(true); } - if (authorTrim.length > 20) { - setAuthorHelperText('저자명은 30자 이내여야 합니다.'); + if (authorTrim.length > 30) { + setAuthorHelperText(COMMON.error.maxLength(30)); setIsInvalidAuthor(true); } diff --git a/src/pages/admin/LibraryManage/Selector/TotalBookNumberSelector.tsx b/src/pages/admin/LibraryManage/Selector/TotalBookNumberSelector.tsx index ee49c0898..cad212787 100644 --- a/src/pages/admin/LibraryManage/Selector/TotalBookNumberSelector.tsx +++ b/src/pages/admin/LibraryManage/Selector/TotalBookNumberSelector.tsx @@ -8,18 +8,7 @@ interface TotalBookNumberProps { } const TotalBookNumberSelector = ({ value, setValue }: TotalBookNumberProps) => { - const bookNumberList = [ - { id: 1, content: '1권' }, - { id: 2, content: '2권' }, - { id: 3, content: '3권' }, - { id: 4, content: '4권' }, - { id: 5, content: '5권' }, - { id: 6, content: '6권' }, - { id: 7, content: '7권' }, - { id: 8, content: '8권' }, - { id: 9, content: '9권' }, - { id: 10, content: '10권' }, - ]; + const bookNumberList = Array.from({ length: 20 }, (_, index) => ({ id: index + 1, content: `${index + 1}권` })); const handleTotalBookNumberChange = (event: SelectChangeEvent) => { setValue(Number(event.target.value as string)); diff --git a/src/pages/board/BoardView/Modal/SecretPostModal.tsx b/src/pages/board/BoardView/Modal/SecretPostModal.tsx index febb863c0..320dd86e4 100644 --- a/src/pages/board/BoardView/Modal/SecretPostModal.tsx +++ b/src/pages/board/BoardView/Modal/SecretPostModal.tsx @@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { Typography } from '@mui/material'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { BOARD_MSG, COMMON } from '@constants/helperText'; import { POST_PASSWORD_MAX_LENGTH } from '@pages/board/BoardWrite/Modal/SettingUploadModal'; import StandardInput from '@components/Input/StandardInput'; import ActionModal from '@components/Modal/ActionModal'; @@ -49,10 +49,10 @@ const SecretPostModal = ({ setPassword, setIsSecretPasswordSubmited, open, setOp defaultValue="" control={control} rules={{ - required: `작성자가 아닐 시 ${REQUIRE_ERROR_MSG}`, + required: BOARD_MSG.error.requiredPassword, maxLength: { value: POST_PASSWORD_MAX_LENGTH, - message: `비밀번호는 최대 ${POST_PASSWORD_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(POST_PASSWORD_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => { diff --git a/src/pages/board/BoardWrite/BoardWrite.tsx b/src/pages/board/BoardWrite/BoardWrite.tsx index 5422ce7b7..c03a112db 100644 --- a/src/pages/board/BoardWrite/BoardWrite.tsx +++ b/src/pages/board/BoardWrite/BoardWrite.tsx @@ -13,7 +13,7 @@ import { useEditPostThumbnailMutation, useUploadPostMutation, } from '@api/postApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON } from '@constants/helperText'; import memberState from '@recoil/member.recoil'; import { categoryNameToId } from '@utils/converter'; import OutlinedButton from '@components/Button/OutlinedButton'; @@ -74,7 +74,7 @@ const BoardWrite = () => { if (!content || content.length < 0) { setHasContent(false); - setContentErrMsg(REQUIRE_ERROR_MSG); + setContentErrMsg(COMMON.error.required); return; } setHasContent(true); @@ -172,10 +172,10 @@ const BoardWrite = () => { defaultValue={editMode ? editMode.post.title : ''} control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: POST_TITLE_MAX_LENGTH, - message: `제목은 최대 ${POST_TITLE_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(POST_TITLE_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => { diff --git a/src/pages/board/BoardWrite/Modal/SettingUploadModal.tsx b/src/pages/board/BoardWrite/Modal/SettingUploadModal.tsx index a8507fb50..14a8d3618 100644 --- a/src/pages/board/BoardWrite/Modal/SettingUploadModal.tsx +++ b/src/pages/board/BoardWrite/Modal/SettingUploadModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { Checkbox, FormControlLabel, FormGroup, Typography, useMediaQuery, useTheme } from '@mui/material'; import { UploadPostSettings, PostInfo } from '@api/dto'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON } from '@constants/helperText'; import StandardInput from '@components/Input/StandardInput'; import ActionModal from '@components/Modal/ActionModal'; import ImageUploader from '@components/Uploader/ImageUploader'; @@ -118,10 +118,10 @@ const SettingUploadModal = ({ defaultValue="" control={control} rules={{ - required: REQUIRE_ERROR_MSG, + required: COMMON.error.required, maxLength: { value: POST_PASSWORD_MAX_LENGTH, - message: `비밀번호는 최대 ${POST_PASSWORD_MAX_LENGTH}글자 입력이 가능합니다.`, + message: COMMON.error.maxLength(POST_PASSWORD_MAX_LENGTH), }, }} render={({ field, fieldState: { error } }) => { diff --git a/src/pages/home/Trendings.tsx b/src/pages/home/Trendings.tsx index 795025165..88605b70d 100644 --- a/src/pages/home/Trendings.tsx +++ b/src/pages/home/Trendings.tsx @@ -12,8 +12,8 @@ const Card = ({ post }: { post: TrendingPostInfo }) => {
    diff --git a/src/pages/login/Search/SearchPWFirstStep.tsx b/src/pages/login/Search/SearchPWFirstStep.tsx index 4756b7177..d495db9d5 100644 --- a/src/pages/login/Search/SearchPWFirstStep.tsx +++ b/src/pages/login/Search/SearchPWFirstStep.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Divider, Typography, useMediaQuery, useTheme } from '@mui/material'; import { DateTime } from 'luxon'; import { useCheckAuthCodeMutation, useRequestAuthCodeMutation } from '@api/SearchAccountApi'; -import { REQUIRE_ERROR_MSG } from '@constants/errorMsg'; +import { COMMON, EMAIL_MSG } from '@constants/helperText'; import { validateEmail } from '@utils/validateEmail'; import OutlinedButton from '@components/Button/OutlinedButton'; import EmailAuthInput from '@components/Input/EmailAuthInput'; @@ -64,17 +64,17 @@ const SearchPWFirstStep = ({ setCurrentStep, form, setForm }: SearchPWFirstStepP const handleEmailBlur = () => { if (!validateEmail(form.email)) { - setEmailErrorMsg('이메일 형식을 확인해주세요.'); + setEmailErrorMsg(EMAIL_MSG.error.formatError); } }; const handleRequestVerificationCode = () => { if (!form.id) { - setIdErrorMsg(REQUIRE_ERROR_MSG); + setIdErrorMsg(COMMON.error.required); return; } if (!form.email) { - setEmailErrorMsg(REQUIRE_ERROR_MSG); + setEmailErrorMsg(COMMON.error.required); return; } diff --git a/src/pages/login/Search/SearchPWSecondStep.tsx b/src/pages/login/Search/SearchPWSecondStep.tsx index 151be0191..80b78ddf1 100644 --- a/src/pages/login/Search/SearchPWSecondStep.tsx +++ b/src/pages/login/Search/SearchPWSecondStep.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Divider } from '@mui/material'; import { useChangePasswordMutation } from '@api/SearchAccountApi'; +import { CONFIRM_PASSWORD_MSG } from '@constants/helperText'; import OutlinedButton from '@components/Button/OutlinedButton'; import StandardInput from '@components/Input/StandardInput'; @@ -45,11 +46,11 @@ const SearchPWSecondStep = ({ setCurrentStep, firstForm }: SearchPWSecondStepPro const handleBlur = (e: React.FocusEvent) => { const { name, value } = e.currentTarget; if (name === 'newPassword') { - if (value && !passwordRegex.test(value)) setPasswordErrorMsg('8~20자 영문과 숫자를 사용하세요.'); + if (value && !passwordRegex.test(value)) setPasswordErrorMsg(CONFIRM_PASSWORD_MSG.error.formatError); } if (name === 'confirmPassword') { if (!(value.length > 0 && isSame)) { - setConfirmPasswordErrorMsg('비밀번호가 일치하지 않습니다.'); + setConfirmPasswordErrorMsg(CONFIRM_PASSWORD_MSG.error.mismatch); } } }; @@ -112,7 +113,7 @@ const SearchPWSecondStep = ({ setCurrentStep, firstForm }: SearchPWSecondStepPro onChange={handleChange} onBlur={handleBlur} error={Boolean(confirmPasswordErrorMsg)} - helperText={confirmPasswordErrorMsg || (isSame && '비밀번호가 일치합니다.')} + helperText={confirmPasswordErrorMsg || (isSame && CONFIRM_PASSWORD_MSG.success.match)} />
    diff --git a/src/pages/senimarAttend/Card/MemberCardContent.tsx b/src/pages/senimarAttend/Card/MemberCardContent.tsx index c8ead08ea..0e3ed25c7 100644 --- a/src/pages/senimarAttend/Card/MemberCardContent.tsx +++ b/src/pages/senimarAttend/Card/MemberCardContent.tsx @@ -5,6 +5,7 @@ import { DateTime } from 'luxon'; import { useRecoilState } from 'recoil'; import { SeminarStatus } from '@api/dto'; import { useAttendSeminarMutation, useGetAvailableSeminarInfoQuery, useGetSeminarInfoQuery } from '@api/seminarApi'; +import { MEMBER_CARD } from '@constants/apiResponseMessage'; import FilledButton from '@components/Button/FilledButton'; import ConfirmModal from '@components/Modal/ConfirmModal'; import Countdown from '../Countdown/Countdown'; @@ -51,10 +52,10 @@ const MemberCardContent = ({ seminarId }: { seminarId: number }) => { if (remainAttendCount <= 0) { setExcessModalOpen(true); - setIncorrectCodeMsg('남은 제출 횟수가 없습니다.'); + setIncorrectCodeMsg(MEMBER_CARD.error.noSubmissionsLeft); return; } - setIncorrectCodeMsg(`출석코드가 틀렸습니다. (남은 제출횟수 ${remainAttendCount}회)`); + setIncorrectCodeMsg(MEMBER_CARD.error.mismatchWithCount(remainAttendCount)); return; } const errorMessage = axiosError?.response?.data?.message; diff --git a/src/pages/senimarAttend/SenimarAttend.tsx b/src/pages/senimarAttend/SenimarAttend.tsx index 0b650e475..4ce8edaa2 100644 --- a/src/pages/senimarAttend/SenimarAttend.tsx +++ b/src/pages/senimarAttend/SenimarAttend.tsx @@ -8,6 +8,7 @@ import { useGetRecentlyDoneSeminarInfoQuery, useGetRecentlyUpcomingSeminarInfoQuery, } from '@api/seminarApi'; +import { MEMBER_ROLE } from '@constants/member'; import useCheckAuth from '@hooks/useCheckAuth'; import memberState from '@recoil/member.recoil'; import starterState from '@recoil/seminarStarter.recoil'; @@ -30,7 +31,7 @@ const SeminarAttend = () => { const { data: availableSeminarData } = useGetAvailableSeminarInfoQuery(); const { checkIncludeOneOfAuths } = useCheckAuth(); - const authorizedMember = checkIncludeOneOfAuths(['ROLE_회장', 'ROLE_부회장', 'ROLE_서기']); + const authorizedMember = checkIncludeOneOfAuths([MEMBER_ROLE.회장, MEMBER_ROLE.부회장, MEMBER_ROLE.서기]); const startMember: number | undefined = useRecoilValue(starterState); const member: MemberInfo | null = useRecoilValue(memberState); diff --git a/tailwind.config.js b/tailwind.config.js index a59598de0..85aa1a2ee 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,64 +1,63 @@ /** @type {import('tailwindcss').Config} */ +import { KEEPER_HEIGHT, KEEPER_COLOR, KEEPER_WIDTH } from './src/constants/keeperTheme'; -module.exports = { - content: ['./src/**/*.{js,jsx,ts,tsx}'], - theme: { - extend: { - colors: { - mainBlack: '#131316', - middleBlack: '#18181C', - subBlack: '#26262C', - pointBlue: '#4CEEF9', - subGray: '#575E69', - subRed: '#EF4444', - subOrange: '#FFA500', - }, - fontFamily: { - base: 'IBM Plex Sans KR, system-ui, sans-serif', - orbitron: '"Orbitron", sans-serif', - }, - fontSize: { - h1: '28px', - h3: '20px', - paragraph: '14px', - small: '10px', - }, - spacing: { - header: '66px', - sidebar: '240px', - }, - maxWidth: { - container: '1080px', - }, - minWidth: { - sidebar: '240px', - }, - backgroundImage: { - galaxy: "url('/public/img/background_galaxy.png')", - }, - keyframes: { - typing: { - '0%': { - width: '0%', - visibility: 'hidden', - }, - '100%': { - width: '100%', - }, +export const content = ['./src/**/*.{js,jsx,ts,tsx}']; +export const theme = { + extend: { + colors: { + mainBlack: KEEPER_COLOR.mainBlack, + middleBlack: KEEPER_COLOR.middleBlack, + subBlack: KEEPER_COLOR.subBlack, + pointBlue: KEEPER_COLOR.pointBlue, + subGray: KEEPER_COLOR.subGray, + subRed: KEEPER_COLOR.subRed, + subOrange: KEEPER_COLOR.subOrange, + }, + fontFamily: { + base: 'IBM Plex Sans KR, system-ui, sans-serif', + orbitron: '"Orbitron", sans-serif', + }, + fontSize: { + h1: '28px', + h3: '20px', + paragraph: '14px', + small: '10px', + }, + spacing: { + header: KEEPER_HEIGHT.header.sm, + sidebar: KEEPER_WIDTH.sidebar, + }, + maxWidth: { + container: KEEPER_WIDTH.container, + }, + minWidth: { + sidebar: KEEPER_WIDTH.sidebar, + }, + backgroundImage: { + galaxy: "url('/public/img/background_galaxy.png')", + }, + keyframes: { + typing: { + '0%': { + width: '0%', + visibility: 'hidden', }, - blink: { - '50%': { - borderColor: 'transparent', - }, - '100%': { - borderColor: 'white', - }, + '100%': { + width: '100%', }, }, - animation: { - typing: 'typing 2s steps(25), blink', + blink: { + '50%': { + borderColor: 'transparent', + }, + '100%': { + borderColor: 'white', + }, }, }, + animation: { + typing: 'typing 2s steps(25), blink', + }, }, - plugins: [], }; +export const plugins = [];