From 984a67afeeb331a003e9bec55b2d6d10e2c3e393 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Mon, 10 Oct 2022 22:15:06 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[FE]=20issue387:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=9F=B0=ED=83=80=EC=9E=84=EC=97=90=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=ED=99=95=EC=9D=B8=20(#401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: is함수 typeChecker로 통합 * refactor: handlers 폴더로 묶기 * feat: typeChecker 구현 * feat: json 파일 수정 * feat: handler 수정 * feat: tag type check * feat: member type check * feat: link type check * feat: link-preview type check * feat: auth type check * feat: community & notice article type check * feat: review type check * feat: study, my study type check * feat: Owner type 수정 * feat: axios response interceptor 수정 아무 응답도 오지 않을 시 빈 string('')이 응답 데이터로 반환됨 -> 명시적으로 null로 교체 * refactor: TODO 주석 추가 * refactor: checker 타입 수정 및 함수 위치 이동 - 도메인에 맞게 이동 * refactor: 타입 이름 변경 및 import 'type' 추가 * refactor: axiosInstance interceptor 함수 분리 * feat: util typeChecker axios 의존성 제거 * refactor: my study 함수명 및 타입명 수정 * feat: checkOptionalType과 checkType 합치기 * feat: link preview 타입 수정 * refactor: util typeChecker 수정 * refactor: 상수 분리 * feat: key 배열이 객체의 모든 key를 담고 있는지 확인 * feat: object.hasOwnProperties -> Object.hasOwn - https://eslint.org/docs/latest/rules/no-prototype-builtins - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn * fix: 라우팅 오류 수정 * refactor: 상수 분리 - USER_ROLE - CATEGORY_NAME - STUDY_STATUS * feat: checkType 함수 수정 isNullOrUndefined -> isNull --- frontend/src/App.tsx | 2 +- frontend/src/api/auth/index.ts | 12 ++- frontend/src/api/auth/typeChecker.ts | 37 +++++++ frontend/src/api/axiosInstance.ts | 15 ++- frontend/src/api/community/index.ts | 24 ++--- frontend/src/api/community/typeChecker.ts | 44 +++++++++ frontend/src/api/link-preview/index.ts | 12 ++- frontend/src/api/link-preview/typeChecker.ts | 23 +++++ frontend/src/api/link/index.ts | 8 +- frontend/src/api/link/typeChecker.ts | 27 ++++++ frontend/src/api/links/index.ts | 3 +- frontend/src/api/links/typeChecker.ts | 22 +++++ frontend/src/api/member/index.ts | 5 +- frontend/src/api/member/typeChecker.ts | 62 ++++++++++++ frontend/src/api/my-studies/index.ts | 3 +- frontend/src/api/my-studies/typeChecker.ts | 16 +++ frontend/src/api/my-study/index.ts | 6 +- frontend/src/api/my-study/typeChecker.ts | 55 +++++++++++ frontend/src/api/notice/index.ts | 13 ++- frontend/src/api/notice/typeChecker.ts | 44 +++++++++ frontend/src/api/review/index.ts | 8 +- frontend/src/api/review/typeChecker.ts | 26 +++++ frontend/src/api/reviews/index.ts | 3 +- frontend/src/api/reviews/typeChecker.ts | 21 ++++ frontend/src/api/studies/index.ts | 3 +- frontend/src/api/studies/typeChecker.ts | 22 +++++ frontend/src/api/study/index.ts | 9 +- frontend/src/api/study/typeChecker.ts | 91 ++++++++++++++++++ frontend/src/api/tags/index.ts | 3 +- frontend/src/api/tags/typeChecker.ts | 54 +++++++++++ .../assets/images/moamoa-site-image.png} | Bin .../multi-tag-select/MultiTagSelect.tsx | 3 +- frontend/src/constants.ts | 23 +++++ frontend/src/custom-types/index.ts | 21 ++-- frontend/src/mocks/browser.ts | 2 +- .../mocks/{ => handlers}/communityHandler.ts | 4 +- .../{ => handlers}/detailStudyHandlers.ts | 17 ++-- frontend/src/mocks/{ => handlers}/handlers.ts | 18 ++-- .../src/mocks/{ => handlers}/linkHandlers.ts | 2 +- .../mocks/{ => handlers}/memberHandlers.ts | 0 .../src/mocks/{ => handlers}/myHandlers.ts | 0 .../src/mocks/{ => handlers}/noticeHandler.ts | 2 +- .../src/mocks/{ => handlers}/reviewHandler.ts | 2 +- .../src/mocks/{ => handlers}/tagHandlers.ts | 0 .../src/mocks/{ => handlers}/tokenHandlers.ts | 0 frontend/src/mocks/reviews.json | 40 ++++---- frontend/src/mocks/studies.json | 10 +- .../components/category/Category.tsx | 4 +- .../components/head/Head.stories.tsx | 4 +- .../detail-page/components/head/Head.tsx | 4 +- .../study-float-box/StudyFloatBox.tsx | 8 +- .../StudyWideFloatBox.tsx | 8 +- .../pages/edit-study-page/EditStudyPage.tsx | 8 +- frontend/src/pages/main-page/MainPage.tsx | 4 +- .../my-study-page/hooks/useMyStudyPage.ts | 8 +- .../pages/study-room-page/StudyRoomPage.tsx | 4 +- .../components/link-item/LinkItem.tsx | 10 +- frontend/src/utils/arrayOfAll.ts | 13 +++ frontend/src/utils/dates.ts | 7 +- frontend/src/utils/index.ts | 8 +- frontend/src/utils/isFunction.ts | 7 -- frontend/src/utils/isNullOrUndefined.ts | 3 - frontend/src/utils/isObject.ts | 6 -- frontend/src/utils/type-checks.ts | 8 -- frontend/src/utils/typeChecker.ts | 72 ++++++++++++++ 65 files changed, 831 insertions(+), 172 deletions(-) create mode 100644 frontend/src/api/auth/typeChecker.ts create mode 100644 frontend/src/api/community/typeChecker.ts create mode 100644 frontend/src/api/link-preview/typeChecker.ts create mode 100644 frontend/src/api/link/typeChecker.ts create mode 100644 frontend/src/api/links/typeChecker.ts create mode 100644 frontend/src/api/member/typeChecker.ts create mode 100644 frontend/src/api/my-studies/typeChecker.ts create mode 100644 frontend/src/api/my-study/typeChecker.ts create mode 100644 frontend/src/api/notice/typeChecker.ts create mode 100644 frontend/src/api/review/typeChecker.ts create mode 100644 frontend/src/api/reviews/typeChecker.ts create mode 100644 frontend/src/api/studies/typeChecker.ts create mode 100644 frontend/src/api/study/typeChecker.ts create mode 100644 frontend/src/api/tags/typeChecker.ts rename frontend/{public/open-graph-image.png => src/assets/images/moamoa-site-image.png} (100%) rename frontend/src/mocks/{ => handlers}/communityHandler.ts (97%) rename frontend/src/mocks/{ => handlers}/detailStudyHandlers.ts (80%) rename frontend/src/mocks/{ => handlers}/handlers.ts (75%) rename frontend/src/mocks/{ => handlers}/linkHandlers.ts (98%) rename frontend/src/mocks/{ => handlers}/memberHandlers.ts (100%) rename frontend/src/mocks/{ => handlers}/myHandlers.ts (100%) rename frontend/src/mocks/{ => handlers}/noticeHandler.ts (98%) rename frontend/src/mocks/{ => handlers}/reviewHandler.ts (97%) rename frontend/src/mocks/{ => handlers}/tagHandlers.ts (100%) rename frontend/src/mocks/{ => handlers}/tokenHandlers.ts (100%) create mode 100644 frontend/src/utils/arrayOfAll.ts delete mode 100644 frontend/src/utils/isFunction.ts delete mode 100644 frontend/src/utils/isNullOrUndefined.ts delete mode 100644 frontend/src/utils/isObject.ts delete mode 100644 frontend/src/utils/type-checks.ts create mode 100644 frontend/src/utils/typeChecker.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6769ccad..54d9a4bbc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -52,7 +52,7 @@ const App = () => { } /> }> {/* TODO: 인덱스 페이지를 따로 두면 좋을 것 같다. */} - } /> + } /> }> {[PATH.NOTICE_PUBLISH, PATH.NOTICE_ARTICLE(), PATH.NOTICE_EDIT()].map((path, index) => ( } /> diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index 13c32004c..1c3c31559 100644 --- a/frontend/src/api/auth/index.ts +++ b/frontend/src/api/auth/index.ts @@ -1,6 +1,7 @@ -import { type AxiosError, type AxiosResponse } from 'axios'; +import { AxiosError, type AxiosResponse } from 'axios'; import { useMutation } from 'react-query'; +import { checkLogin, checkRefresh } from '@api/auth/typeChecker'; import axiosInstance, { refreshAxiosInstance } from '@api/axiosInstance'; export type ApiLogin = { @@ -14,7 +15,7 @@ export type ApiLogin = { }; }; -export type ApiRefreshToken = { +export type ApiRefresh = { get: { responseData: { accessToken: string; @@ -30,7 +31,8 @@ export const postLogin = async ({ code }: ApiLogin['post']['variables']) => { AxiosResponse, ApiLogin['post']['variables'] >(`/api/auth/login?code=${code}`); - return response.data; + + return checkLogin(response.data); }; export const usePostLogin = () => @@ -38,6 +40,6 @@ export const usePostLogin = () => // refresh - get new access token export const getRefreshAccessToken = async () => { - const response = await refreshAxiosInstance.get(`/api/auth/refresh`); - return response.data; + const response = await refreshAxiosInstance.get(`/api/auth/refresh`); + return checkRefresh(response.data); }; diff --git a/frontend/src/api/auth/typeChecker.ts b/frontend/src/api/auth/typeChecker.ts new file mode 100644 index 000000000..9a098da31 --- /dev/null +++ b/frontend/src/api/auth/typeChecker.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isNumber, isObject, isString } from '@utils'; + +import { type ApiLogin, type ApiRefresh } from '@api/auth'; + +type LoginKeys = keyof ApiLogin['post']['responseData']; + +const arrayOfAllLoginKeys = arrayOfAll(); + +export const checkLogin = (data: unknown): ApiLogin['post']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Login does not have correct type: object`); + + const keys = arrayOfAllLoginKeys(['accessToken', 'expiredTime']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Login does not have some properties'); + + return { + accessToken: checkType(data.accessToken, isString), + expiredTime: checkType(data.expiredTime, isNumber), + }; +}; + +type RefreshKeys = keyof ApiRefresh['get']['responseData']; + +const arrayOfAllRefreshKeys = arrayOfAll(); + +export const checkRefresh = (data: unknown): ApiRefresh['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Refresh does not have correct type: object`); + + const keys = arrayOfAllRefreshKeys(['accessToken', 'expiredTime']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Refresh does not have some properties'); + + return { + accessToken: checkType(data.accessToken, isString), + expiredTime: checkType(data.expiredTime, isNumber), + }; +}; diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index 2e99cfc66..8791e4dfa 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,5 +1,4 @@ -import axios from 'axios'; -import type { AxiosError } from 'axios'; +import axios, { type AxiosError, type AxiosResponse } from 'axios'; import { getRefreshAccessToken } from '@api/auth'; @@ -35,8 +34,16 @@ const handleAxiosError = (error: AxiosError<{ message: string; code?: number }>) return Promise.reject(error); }; -axiosInstance.interceptors.response.use(response => response, handleAxiosError); -refreshAxiosInstance.interceptors.response.use(response => response, handleAxiosError); +const handleAxiosResponse = (response: AxiosResponse) => { + // 서버에서 아무 응답 데이터도 오지 않으면 빈 스트링 ''이 오므로 명시적으로 null로 지정 + if (response.data !== '') return response; + + response.data = null; + return response; +}; + +axiosInstance.interceptors.response.use(handleAxiosResponse, handleAxiosError); +refreshAxiosInstance.interceptors.response.use(handleAxiosResponse, handleAxiosError); axiosInstance.interceptors.request.use( async config => { diff --git a/frontend/src/api/community/index.ts b/frontend/src/api/community/index.ts index 5954c8e80..66ed4d993 100644 --- a/frontend/src/api/community/index.ts +++ b/frontend/src/api/community/index.ts @@ -1,9 +1,12 @@ -import { type AxiosError, type AxiosResponse } from 'axios'; +import { AxiosError, type AxiosResponse } from 'axios'; import { useMutation, useQuery } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { ArticleId, CommunityArticle, Page, Size, StudyId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkCommunityArticle, checkCommunityArticles } from '@api/community/typeChecker'; export type ApiCommunityArticles = { get: { @@ -60,16 +63,8 @@ const getCommunityArticles = async ({ studyId, page = 1, size = 8 }: ApiCommunit const response = await axiosInstance.get( `/api/studies/${studyId}/community/articles?page=${page - 1}&size=${size}`, ); - const { totalCount, currentPage, lastPage } = response.data; - - response.data = { - ...response.data, - totalCount: Number(totalCount), - currentPage: Number(currentPage) + 1, // page를 하나 늘려준다 서버에서 0으로 오기 때문이다 - lastPage: Number(lastPage), - }; - return response.data; + return checkCommunityArticles(response.data); }; // articles @@ -86,7 +81,8 @@ const getCommunityArticle = async ({ studyId, articleId }: ApiCommunityArticle[' const response = await axiosInstance.get( `/api/studies/${studyId}/community/articles/${articleId}`, ); - return response.data; + + return checkCommunityArticle(response.data); }; export const useGetCommunityArticle = ({ studyId, articleId }: ApiCommunityArticle['get']['variables']) => { @@ -106,7 +102,7 @@ const postCommunityArticle = async ({ studyId, title, content }: ApiCommunityArt }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePostCommunityArticle = () => { @@ -123,7 +119,7 @@ const putCommunityArticle = async ({ studyId, title, content, articleId }: ApiCo }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePutCommunityArticle = () => { @@ -136,7 +132,7 @@ const deleteCommunityArticle = async ({ studyId, articleId }: ApiCommunityArticl `/api/studies/${studyId}/community/articles/${articleId}`, ); - return response.data; + return checkType(response.data, isNull); }; export const useDeleteCommunityArticle = () => { diff --git a/frontend/src/api/community/typeChecker.ts b/frontend/src/api/community/typeChecker.ts new file mode 100644 index 000000000..203e7a14b --- /dev/null +++ b/frontend/src/api/community/typeChecker.ts @@ -0,0 +1,44 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isArray, isDateYMD, isNumber, isObject, isString } from '@utils'; + +import { type ApiCommunityArticle, type ApiCommunityArticles } from '@api/community'; +import { checkMember } from '@api/member/typeChecker'; + +type CommunityArticleKeys = keyof ApiCommunityArticle['get']['responseData']; + +const arrayOfAllCommunityArticleKeys = arrayOfAll(); + +export const checkCommunityArticle = (data: unknown): ApiCommunityArticle['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`CommunityArticle does not have correct type: object`); + + const keys = arrayOfAllCommunityArticleKeys(['id', 'author', 'title', 'content', 'createdDate', 'lastModifiedDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('CommunityArticle does not have some properties'); + + return { + id: checkType(data.id, isNumber), + author: checkMember(data.author), + title: checkType(data.title, isString), + content: checkType(data.content, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), + }; +}; + +type CommunityArticlesKeys = keyof ApiCommunityArticles['get']['responseData']; + +const arrayOfAllCommunityArticlesKeys = arrayOfAll(); + +export const checkCommunityArticles = (data: unknown): ApiCommunityArticles['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`CommunityArticles does not have correct type: object`); + + const keys = arrayOfAllCommunityArticlesKeys(['articles', 'currentPage', 'lastPage', 'totalCount']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('CommunityArticles does not have some properties'); + + return { + articles: checkType(data.articles, isArray).map(article => checkCommunityArticle(article)), + currentPage: checkType(data.currentPage, isNumber) + 1, + lastPage: checkType(data.lastPage, isNumber), + totalCount: checkType(data.totalCount, isNumber), + }; +}; diff --git a/frontend/src/api/link-preview/index.ts b/frontend/src/api/link-preview/index.ts index 54157ff35..2ab417847 100644 --- a/frontend/src/api/link-preview/index.ts +++ b/frontend/src/api/link-preview/index.ts @@ -2,6 +2,8 @@ import axios from 'axios'; import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; +import { checkLinkPreview } from '@api/link-preview/typeChecker'; + import AccessTokenController from '@auth/accessTokenController'; export type ApiLinkPreview = { @@ -10,10 +12,10 @@ export type ApiLinkPreview = { linkUrl: string; }; responseData: { - title: string | null; - description: string | null; - imageUrl: string | null; - domainName: string | null; + title?: string; + description?: string; + imageUrl?: string; + domainName?: string; }; variables: ApiLinkPreview['get']['params']; }; @@ -65,7 +67,7 @@ export const getLinkPreview = async ({ linkUrl }: ApiLinkPreview['get']['variabl const response = await axiosInstance.get( `/api/link-preview?linkUrl=${linkUrl}`, ); - return response.data; + return checkLinkPreview(response.data); }; export const useGetLinkPreview = ({ linkUrl }: ApiLinkPreview['get']['variables']) => { diff --git a/frontend/src/api/link-preview/typeChecker.ts b/frontend/src/api/link-preview/typeChecker.ts new file mode 100644 index 000000000..d3d3ac4f2 --- /dev/null +++ b/frontend/src/api/link-preview/typeChecker.ts @@ -0,0 +1,23 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isObject, isString } from '@utils'; + +import { type ApiLinkPreview } from '@api/link-preview'; + +type LinkPreviewKeys = keyof ApiLinkPreview['get']['responseData']; + +const arrayOfAllLinkPreviewKeys = arrayOfAll(); + +export const checkLinkPreview = (data: unknown): ApiLinkPreview['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`LinkPreview does not have correct type: object`); + + const keys = arrayOfAllLinkPreviewKeys(['title', 'description', 'imageUrl', 'domainName']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('LinkPreview does not have some properties'); + + return { + title: checkType(data.title, isString, true), + description: checkType(data.description, isString, true), + imageUrl: checkType(data.imageUrl, isString, true), + domainName: checkType(data.domainName, isString, true), + }; +}; diff --git a/frontend/src/api/link/index.ts b/frontend/src/api/link/index.ts index ef2dd177b..f75ac7516 100644 --- a/frontend/src/api/link/index.ts +++ b/frontend/src/api/link/index.ts @@ -1,6 +1,8 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { useMutation } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { Link, LinkId, StudyId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; @@ -30,7 +32,7 @@ export const postLink = async ({ studyId, linkUrl, description }: ApiLink['post' description, }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePostLink = () => useMutation(postLink); @@ -45,7 +47,7 @@ export const putLink = async ({ studyId, linkId, linkUrl, description }: ApiLink description, }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePutLink = () => useMutation(putLink); @@ -54,7 +56,7 @@ export const deleteLink = async ({ studyId, linkId }: ApiLink['delete']['variabl const response = await axiosInstance.delete, null>( `/api/studies/${studyId}/reference-room/links/${linkId}`, ); - return response.data; + return checkType(response.data, isNull); }; export const useDeleteLink = () => useMutation(deleteLink); diff --git a/frontend/src/api/link/typeChecker.ts b/frontend/src/api/link/typeChecker.ts new file mode 100644 index 000000000..f48541599 --- /dev/null +++ b/frontend/src/api/link/typeChecker.ts @@ -0,0 +1,27 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isDateYMD, isNumber, isObject, isString } from '@utils'; + +import type { Link } from '@custom-types'; + +import { checkMember } from '@api/member/typeChecker'; + +type LinkKeys = keyof Link; + +const arrayOfAllLinkKeys = arrayOfAll(); + +export const checkLink = (data: unknown): Link => { + if (!isObject(data)) throw new AxiosError(`Link does not have correct type: object`); + + const keys = arrayOfAllLinkKeys(['id', 'author', 'linkUrl', 'description', 'createdDate', 'lastModifiedDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Link does not have some properties'); + + return { + id: checkType(data.id, isNumber), + author: checkMember(data.author), + linkUrl: checkType(data.linkUrl, isString), + description: checkType(data.description, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.createdDate, isDateYMD), + }; +}; diff --git a/frontend/src/api/links/index.ts b/frontend/src/api/links/index.ts index c3d77ae9c..ccf0362d8 100644 --- a/frontend/src/api/links/index.ts +++ b/frontend/src/api/links/index.ts @@ -6,6 +6,7 @@ import { DEFAULT_LINK_QUERY_PARAM } from '@constants'; import type { Link, Page, Size, StudyId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkLinks } from '@api/links/typeChecker'; export type ApiLinks = { get: { @@ -41,7 +42,7 @@ export const getLinks = async ({ studyId, page, size }: ApiLinks['get']['variabl const response = await axiosInstance.get( `/api/studies/${studyId}/reference-room/links?page=${page}&size=${size}`, ); - return response.data; + return checkLinks(response.data); }; const getLinksWithPage = diff --git a/frontend/src/api/links/typeChecker.ts b/frontend/src/api/links/typeChecker.ts new file mode 100644 index 000000000..05a63008d --- /dev/null +++ b/frontend/src/api/links/typeChecker.ts @@ -0,0 +1,22 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isArray, isBoolean, isObject } from '@utils'; + +import { checkLink } from '@api/link/typeChecker'; +import { type ApiLinks } from '@api/links'; + +type LinksKeys = keyof ApiLinks['get']['responseData']; + +const arrayOfAllLinksKeys = arrayOfAll(); + +export const checkLinks = (data: unknown): ApiLinks['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Links does not have correct type: object`); + + const keys = arrayOfAllLinksKeys(['links', 'hasNext']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Links does not have some properties'); + + return { + links: checkType(data.links, isArray).map(link => checkLink(link)), + hasNext: checkType(data.hasNext, isBoolean), + }; +}; diff --git a/frontend/src/api/member/index.ts b/frontend/src/api/member/index.ts index 13d18a2e6..32a66d5da 100644 --- a/frontend/src/api/member/index.ts +++ b/frontend/src/api/member/index.ts @@ -4,6 +4,7 @@ import { QueryKey, UseQueryOptions, useQuery } from 'react-query'; import type { Member, StudyId, UserRole } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkUserInformation, checkUserRole } from '@api/member/typeChecker'; export type ApiUserRole = { get: { @@ -32,7 +33,7 @@ export const getUserRole = async ({ studyId }: ApiUserRole['get']['variables']) const response = await axiosInstance.get( `/api/members/me/role?study-id=${studyId}`, ); - return response.data; + return checkUserRole(response.data); }; export const useGetUserRole = ({ studyId, options }: ApiUserRole['get']['variables']) => @@ -44,7 +45,7 @@ export const useGetUserRole = ({ studyId, options }: ApiUserRole['get']['variabl export const getUserInformation = async () => { const response = await axiosInstance.get(`/api/members/me`); - return response.data; + return checkUserInformation(response.data); }; export const useGetUserInformation = () => diff --git a/frontend/src/api/member/typeChecker.ts b/frontend/src/api/member/typeChecker.ts new file mode 100644 index 000000000..1dc69a889 --- /dev/null +++ b/frontend/src/api/member/typeChecker.ts @@ -0,0 +1,62 @@ +import { AxiosError } from 'axios'; + +import { + arrayOfAll, + checkType, + hasOwnProperties, + hasOwnProperty, + isNumber, + isObject, + isString, + isUserRole, +} from '@utils'; + +import type { Member } from '@custom-types'; + +import { type ApiUserInformation, type ApiUserRole } from '@api/member'; + +type UserInformationKeys = keyof ApiUserInformation['get']['responseData']; + +const arrayOfAllUserInformationKeys = arrayOfAll(); + +export const checkUserInformation = (data: unknown): ApiUserInformation['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`UserInformation does not have correct type: object`); + + const keys = arrayOfAllUserInformationKeys(['id', 'username', 'imageUrl', 'profileUrl']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('UserInformation does not have some properties'); + + return { + id: checkType(data.id, isNumber), + username: checkType(data.username, isString), + imageUrl: checkType(data.imageUrl, isString), + profileUrl: checkType(data.profileUrl, isString), + }; +}; + +export const checkUserRole = (data: unknown): ApiUserRole['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`UserRole does not have correct type: object`); + + if (!hasOwnProperty(data, 'role')) throw new AxiosError('UserRole does not have some properties'); + + return { + role: checkType(data.role, isUserRole), + }; +}; + +type MemberKeys = keyof Member; + +const arrayOfAllMemberKeys = arrayOfAll(); + +export const checkMember = (data: unknown): Member => { + if (!isObject(data)) throw new AxiosError(`Member does not have correct type: object`); + + const keys = arrayOfAllMemberKeys(['id', 'username', 'imageUrl', 'profileUrl']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Member does not have some properties'); + + return { + id: checkType(data.id, isNumber), + username: checkType(data.username, isString), + imageUrl: checkType(data.imageUrl, isString), + profileUrl: checkType(data.profileUrl, isString), + }; +}; diff --git a/frontend/src/api/my-studies/index.ts b/frontend/src/api/my-studies/index.ts index d0912a151..81399dc6f 100644 --- a/frontend/src/api/my-studies/index.ts +++ b/frontend/src/api/my-studies/index.ts @@ -4,6 +4,7 @@ import { useQuery } from 'react-query'; import type { MyStudy } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkMyStudies } from '@api/my-studies/typeChecker'; export const QK_MY_STUDIES = 'my-studies'; @@ -17,7 +18,7 @@ export type ApiMyStudies = { export const getMyStudies = async () => { const response = await axiosInstance.get(`/api/my/studies`); - return response.data; + return checkMyStudies(response.data); }; export const useGetMyStudies = () => { diff --git a/frontend/src/api/my-studies/typeChecker.ts b/frontend/src/api/my-studies/typeChecker.ts new file mode 100644 index 000000000..ba34fdc96 --- /dev/null +++ b/frontend/src/api/my-studies/typeChecker.ts @@ -0,0 +1,16 @@ +import { AxiosError } from 'axios'; + +import { checkType, hasOwnProperty, isArray, isObject } from '@utils'; + +import { type ApiMyStudies } from '@api/my-studies'; +import { checkMyStudy } from '@api/my-study/typeChecker'; + +export const checkMyStudies = (data: unknown): ApiMyStudies['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`MyStudies does not have correct type: object`); + + if (!hasOwnProperty(data, 'studies')) throw new AxiosError('MyStudies does not have some properties'); + + return { + studies: checkType(data.studies, isArray).map(study => checkMyStudy(study)), + }; +}; diff --git a/frontend/src/api/my-study/index.ts b/frontend/src/api/my-study/index.ts index 03532bdd5..c7cd8e4c3 100644 --- a/frontend/src/api/my-study/index.ts +++ b/frontend/src/api/my-study/index.ts @@ -1,6 +1,8 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { useMutation } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { StudyId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; @@ -23,7 +25,7 @@ export const postMyStudy = async ({ studyId }: ApiMyStudy['post']['variables']) const response = await axiosInstance.post, ApiMyStudy['post']['variables']>( `/api/studies/${studyId}/members`, ); - return response.data; + return checkType(response.data, isNull); }; export const usePostMyStudy = () => useMutation(postMyStudy); @@ -32,7 +34,7 @@ export const deleteMyStudy = async ({ studyId }: ApiMyStudy['delete']['variables const response = await axiosInstance.delete, ApiMyStudy['delete']['variables']>( `/api/studies/${studyId}/members`, ); - return response.data; + return checkType(response.data, isNull); }; export const useDeleteMyStudy = () => useMutation(deleteMyStudy); diff --git a/frontend/src/api/my-study/typeChecker.ts b/frontend/src/api/my-study/typeChecker.ts new file mode 100644 index 000000000..6f97f8f17 --- /dev/null +++ b/frontend/src/api/my-study/typeChecker.ts @@ -0,0 +1,55 @@ +import { AxiosError } from 'axios'; + +import { + arrayOfAll, + checkType, + hasOwnProperties, + isArray, + isDateYMD, + isNumber, + isObject, + isString, + isStudyStatus, +} from '@utils'; + +import type { MyStudy, Tag } from '@custom-types'; + +import { checkMember } from '@api/member/typeChecker'; + +type MyStudyTag = Pick; +type MyStudyTagKeys = keyof MyStudyTag; + +const arrayOfAllMyStudyTagKeys = arrayOfAll(); + +const checkMyStudyTag = (data: unknown): MyStudyTag => { + if (!isObject(data)) throw new AxiosError(`Tag does not have correct type: object`); + + const keys = arrayOfAllMyStudyTagKeys(['id', 'name']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Tag does not have some properties'); + + return { + id: checkType(data.id, isNumber), + name: checkType(data.name, isString), + }; +}; + +type MyStudyKeys = keyof MyStudy; + +const arrayOfAllMyStudyKeys = arrayOfAll(); + +export const checkMyStudy = (data: unknown): MyStudy => { + if (!isObject(data)) throw new AxiosError(`MyStudy does not have correct type: object`); + + const keys = arrayOfAllMyStudyKeys(['id', 'title', 'studyStatus', 'tags', 'owner', 'startDate', 'endDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('MyStudy does not have some properties'); + + return { + id: checkType(data.id, isNumber), + title: checkType(data.title, isString), + startDate: checkType(data.startDate, isDateYMD), + endDate: checkType(data.endDate, isDateYMD), + studyStatus: checkType(data.studyStatus, isStudyStatus), + tags: checkType(data.tags, isArray).map(tag => checkMyStudyTag(tag)), + owner: checkMember(data.owner), + }; +}; diff --git a/frontend/src/api/notice/index.ts b/frontend/src/api/notice/index.ts index f4edd4635..36393c9bd 100644 --- a/frontend/src/api/notice/index.ts +++ b/frontend/src/api/notice/index.ts @@ -1,9 +1,12 @@ import { AxiosError, AxiosResponse } from 'axios'; import { useMutation, useQuery } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { ArticleId, NoticeArticle, Page, Size, StudyId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkNoticeArticle, checkNoticeArticles } from '@api/notice/typeChecker'; export type ApiNoticeArticles = { get: { @@ -69,7 +72,7 @@ const getNoticeArticles = async ({ studyId, page = 1, size = 8 }: ApiNoticeArtic lastPage: Number(lastPage), }; - return response.data; + return checkNoticeArticles(response.data); }; const getNoticeArticle = async ({ studyId, articleId }: ApiNoticeArticle['get']['variables']) => { @@ -77,7 +80,7 @@ const getNoticeArticle = async ({ studyId, articleId }: ApiNoticeArticle['get'][ const response = await axiosInstance.get( `/api/studies/${studyId}/notice/articles/${articleId}`, ); - return response.data; + return checkNoticeArticle(response.data); }; const postNoticeArticle = async ({ studyId, title, content }: ApiNoticeArticle['post']['variables']) => { @@ -89,7 +92,7 @@ const postNoticeArticle = async ({ studyId, title, content }: ApiNoticeArticle[' }, ); - return response.data; + return checkType(response.data, isNull); }; const putNoticeArticle = async ({ studyId, title, content, articleId }: ApiNoticeArticle['put']['variables']) => { @@ -101,7 +104,7 @@ const putNoticeArticle = async ({ studyId, title, content, articleId }: ApiNotic }, ); - return response.data; + return checkType(response.data, isNull); }; const deleteNoticeArticle = async ({ studyId, articleId }: ApiNoticeArticle['delete']['variables']) => { @@ -109,7 +112,7 @@ const deleteNoticeArticle = async ({ studyId, articleId }: ApiNoticeArticle['del `/api/studies/${studyId}/notice/articles/${articleId}`, ); - return response.data; + return checkType(response.data, isNull); }; export const useGetNoticeArticles = ({ studyId, page }: ApiNoticeArticles['get']['variables']) => { diff --git a/frontend/src/api/notice/typeChecker.ts b/frontend/src/api/notice/typeChecker.ts new file mode 100644 index 000000000..14b06858f --- /dev/null +++ b/frontend/src/api/notice/typeChecker.ts @@ -0,0 +1,44 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isArray, isDateYMD, isNumber, isObject, isString } from '@utils'; + +import { checkMember } from '@api/member/typeChecker'; +import { type ApiNoticeArticle, type ApiNoticeArticles } from '@api/notice'; + +type NoticeArticleKeys = keyof ApiNoticeArticle['get']['responseData']; + +const arrayOfAllNoticeArticleKeys = arrayOfAll(); + +export const checkNoticeArticle = (data: unknown): ApiNoticeArticle['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`NoticeArticle does not have correct type: object`); + + const keys = arrayOfAllNoticeArticleKeys(['id', 'author', 'title', 'content', 'createdDate', 'lastModifiedDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('NoticeArticle does not have some properties'); + + return { + id: checkType(data.id, isNumber), + author: checkMember(data.author), + title: checkType(data.title, isString), + content: checkType(data.content, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), + }; +}; + +type NoticeArticlesKeys = keyof ApiNoticeArticles['get']['responseData']; + +const arrayOfAllNoticeArticlesKeys = arrayOfAll(); + +export const checkNoticeArticles = (data: unknown): ApiNoticeArticles['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`NoticeArticles does not have correct type: object`); + + const keys = arrayOfAllNoticeArticlesKeys(['articles', 'currentPage', 'lastPage', 'totalCount']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('NoticeArticles does not have some properties'); + + return { + articles: checkType(data.articles, isArray).map(article => checkNoticeArticle(article)), + currentPage: checkType(data.currentPage, isNumber) + 1, + lastPage: checkType(data.lastPage, isNumber), + totalCount: checkType(data.totalCount, isNumber), + }; +}; diff --git a/frontend/src/api/review/index.ts b/frontend/src/api/review/index.ts index 84bebcb1d..0d8354900 100644 --- a/frontend/src/api/review/index.ts +++ b/frontend/src/api/review/index.ts @@ -1,6 +1,8 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { useMutation } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { ReviewId, StudyId, StudyReview } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; @@ -37,7 +39,7 @@ export const postReview = async ({ studyId, content }: ApiReview['post']['variab content, }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePostReview = () => useMutation(postReview); @@ -49,7 +51,7 @@ export const putReview = async ({ studyId, reviewId, content }: ApiReview['put'] content, }, ); - return response.data; + return checkType(response.data, isNull); }; export const usePutReview = () => useMutation(putReview); @@ -58,7 +60,7 @@ export const deleteReview = async ({ studyId, reviewId }: ApiReview['delete']['v const response = await axiosInstance.delete, ApiReview['delete']['variables']>( `/api/studies/${studyId}/reviews/${reviewId}`, ); - return response.data; + return checkType(response.data, isNull); }; export const useDeleteReview = () => useMutation(deleteReview); diff --git a/frontend/src/api/review/typeChecker.ts b/frontend/src/api/review/typeChecker.ts new file mode 100644 index 000000000..06268ec04 --- /dev/null +++ b/frontend/src/api/review/typeChecker.ts @@ -0,0 +1,26 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isDateYMD, isNumber, isObject, isString } from '@utils'; + +import type { StudyReview } from '@custom-types'; + +import { checkMember } from '@api/member/typeChecker'; + +type StudyReviewKeys = keyof StudyReview; + +const arrayOfAllStudyReviewKeys = arrayOfAll(); + +export const checkStudyReview = (data: unknown): StudyReview => { + if (!isObject(data)) throw new AxiosError(`StudyReview does not have correct type: object`); + + const keys = arrayOfAllStudyReviewKeys(['id', 'member', 'createdDate', 'lastModifiedDate', 'content']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('StudyReview does not have some properties'); + + return { + id: checkType(data.id, isNumber), + member: checkMember(data.member), + content: checkType(data.content, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), + }; +}; diff --git a/frontend/src/api/reviews/index.ts b/frontend/src/api/reviews/index.ts index 17f5e2ba5..1a9ca7f04 100644 --- a/frontend/src/api/reviews/index.ts +++ b/frontend/src/api/reviews/index.ts @@ -4,6 +4,7 @@ import { useQuery } from 'react-query'; import type { Size, StudyId, StudyReview } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkStudyReviews } from '@api/reviews/typeChecker'; export const QK_STUDY_REVIEWS = 'study-reviews'; @@ -24,7 +25,7 @@ export type ApiReviews = { export const getStudyReviews = async ({ studyId, size = 8 }: ApiReviews['get']['variables']) => { const url = size ? `/api/studies/${studyId}/reviews?size=${size}` : `/api/studies/${studyId}/reviews`; const response = await axiosInstance.get(url); - return response.data; + return checkStudyReviews(response.data); }; export const useGetStudyReviews = ({ studyId, size }: ApiReviews['get']['variables']) => { diff --git a/frontend/src/api/reviews/typeChecker.ts b/frontend/src/api/reviews/typeChecker.ts new file mode 100644 index 000000000..644f0c991 --- /dev/null +++ b/frontend/src/api/reviews/typeChecker.ts @@ -0,0 +1,21 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isArray, isNumber, isObject } from '@utils'; + +import { checkStudyReview } from '@api/review/typeChecker'; +import { type ApiReviews } from '@api/reviews'; + +type StudyReviewsKeys = keyof ApiReviews['get']['responseData']; + +const arrayOfAllStudyReviewsKeys = arrayOfAll(); +export const checkStudyReviews = (data: unknown): ApiReviews['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`StudyReviews does not have correct type: object`); + + const keys = arrayOfAllStudyReviewsKeys(['reviews', 'totalCount']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('StudyReviews does not have some properties'); + + return { + reviews: checkType(data.reviews, isArray).map(review => checkStudyReview(review)), + totalCount: checkType(data.totalCount, isNumber), + }; +}; diff --git a/frontend/src/api/studies/index.ts b/frontend/src/api/studies/index.ts index a2bf89809..2cb9b797c 100644 --- a/frontend/src/api/studies/index.ts +++ b/frontend/src/api/studies/index.ts @@ -6,6 +6,7 @@ import { DEFAULT_STUDY_CARD_QUERY_PARAM } from '@constants'; import type { Page, Size, Study, TagInfo } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkStudies } from '@api/studies/typeChecker'; export type ApiStudies = { get: { @@ -52,7 +53,7 @@ export const getStudies = async ({ const response = await axiosInstance.get( `/api/studies/search?page=${page}&size=${size}${titleParams}${tagParams}`, ); - return response.data; + return checkStudies(response.data); }; const getStudiesWithPage = diff --git a/frontend/src/api/studies/typeChecker.ts b/frontend/src/api/studies/typeChecker.ts new file mode 100644 index 000000000..771d4db01 --- /dev/null +++ b/frontend/src/api/studies/typeChecker.ts @@ -0,0 +1,22 @@ +import { AxiosError } from 'axios'; + +import { arrayOfAll, checkType, hasOwnProperties, isArray, isBoolean, isObject } from '@utils'; + +import { type ApiStudies } from '@api/studies'; +import { checkStudy } from '@api/study/typeChecker'; + +type StudiesKeys = keyof ApiStudies['get']['responseData']; + +const arrayOfAllStudiesKeys = arrayOfAll(); + +export const checkStudies = (data: unknown): ApiStudies['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Studies does not have correct type: object`); + + const keys = arrayOfAllStudiesKeys(['studies', 'hasNext']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Studies does not have some properties'); + + return { + studies: checkType(data.studies, isArray).map(study => checkStudy(study)), + hasNext: checkType(data.hasNext, isBoolean), + }; +}; diff --git a/frontend/src/api/study/index.ts b/frontend/src/api/study/index.ts index 9d7000a74..7bf9f35be 100644 --- a/frontend/src/api/study/index.ts +++ b/frontend/src/api/study/index.ts @@ -1,9 +1,12 @@ import type { AxiosError, AxiosResponse } from 'axios'; import { useMutation, useQuery } from 'react-query'; +import { checkType, isNull } from '@utils'; + import type { MakeOptional, StudyDetail, StudyId, TagId } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkStudy } from '@api/study/typeChecker'; export const QK_STUDY_DETAIL = 'study-detail'; @@ -40,7 +43,7 @@ export const postStudy = async (newStudy: ApiStudy['post']['variables']) => { `/api/studies`, newStudy, ); - return response.data; + return checkType(response.data, isNull); }; export const usePostStudy = () => { @@ -49,7 +52,7 @@ export const usePostStudy = () => { export const getStudy = async ({ studyId }: ApiStudy['get']['variables']) => { const response = await axiosInstance.get(`/api/studies/${studyId}`); - return response.data; + return checkStudy(response.data); }; export const useGetStudy = ({ studyId }: ApiStudy['get']['variables']) => { @@ -58,7 +61,7 @@ export const useGetStudy = ({ studyId }: ApiStudy['get']['variables']) => { export const putStudy = async ({ studyId, editedStudy }: ApiStudy['put']['variables']) => { const response = await axiosInstance.put>(`/api/studies/${studyId}`, editedStudy); - return response.data; + return checkType(response.data, isNull); }; export const usePutStudy = () => useMutation(putStudy); diff --git a/frontend/src/api/study/typeChecker.ts b/frontend/src/api/study/typeChecker.ts new file mode 100644 index 000000000..983fb7e53 --- /dev/null +++ b/frontend/src/api/study/typeChecker.ts @@ -0,0 +1,91 @@ +import { AxiosError } from 'axios'; + +import { + arrayOfAll, + checkType, + hasOwnProperties, + isArray, + isDateYMD, + isNumber, + isObject, + isRecruitmentStatus, + isString, +} from '@utils'; + +import type { DateYMD, Member } from '@custom-types'; + +import { type ApiStudy } from '@api/study'; +import { checkTag } from '@api/tags/typeChecker'; + +type StudyMember = Member & { participationDate: DateYMD; numberOfStudy: number }; +type StudyMemberKeys = keyof StudyMember; + +const arrayOfAllStudyMemberKeys = arrayOfAll(); + +const checkStudyMember = (data: unknown): StudyMember => { + if (!isObject(data)) throw new AxiosError(`StudyMember does not have correct type: object`); + + const keys = arrayOfAllStudyMemberKeys([ + 'id', + 'username', + 'imageUrl', + 'profileUrl', + 'participationDate', + 'numberOfStudy', + ]); + if (!hasOwnProperties(data, keys)) throw new AxiosError('StudyMember does not have some properties'); + + return { + id: checkType(data.id, isNumber), + username: checkType(data.username, isString), + imageUrl: checkType(data.imageUrl, isString), + profileUrl: checkType(data.profileUrl, isString), + participationDate: checkType(data.participationDate, isDateYMD), + numberOfStudy: checkType(data.numberOfStudy, isNumber), + }; +}; + +type StudyKeys = keyof ApiStudy['get']['responseData']; + +const arrayOfAllStudyKeys = arrayOfAll(); + +export const checkStudy = (data: unknown): ApiStudy['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Study does not have correct type: object`); + + const keys = arrayOfAllStudyKeys([ + 'id', + 'title', + 'excerpt', + 'thumbnail', + 'recruitmentStatus', + 'description', + 'currentMemberCount', + 'maxMemberCount', + 'createdDate', + 'enrollmentEndDate', + 'endDate', + 'startDate', + 'owner', + 'members', + 'tags', + ]); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Study does not have some properties'); + + return { + id: checkType(data.id, isNumber), + title: checkType(data.title, isString), + excerpt: checkType(data.excerpt, isString), + thumbnail: checkType(data.thumbnail, isString), + recruitmentStatus: checkType(data.recruitmentStatus, isRecruitmentStatus), + description: checkType(data.description, isString), + currentMemberCount: checkType(data.id, isNumber), + maxMemberCount: checkType(data.maxMemberCount, isNumber, true), + createdDate: checkType(data.createdDate, isDateYMD), + enrollmentEndDate: checkType(data.enrollmentEndDate, isDateYMD, true), + startDate: checkType(data.startDate, isDateYMD), + endDate: checkType(data.endDate, isDateYMD, true), + owner: checkStudyMember(data.owner), + members: checkType(data.members, isArray).map(member => checkStudyMember(member)), + tags: checkType(data.tags, isArray).map(tag => checkTag(tag)), + }; +}; diff --git a/frontend/src/api/tags/index.ts b/frontend/src/api/tags/index.ts index d3e836ce6..4095b82ec 100644 --- a/frontend/src/api/tags/index.ts +++ b/frontend/src/api/tags/index.ts @@ -4,6 +4,7 @@ import { useQuery } from 'react-query'; import type { Tag } from '@custom-types'; import axiosInstance from '@api/axiosInstance'; +import { checkTags } from '@api/tags/typeChecker'; export type ApiTags = { get: { @@ -15,7 +16,7 @@ export type ApiTags = { export const getTags = async () => { const response = await axiosInstance.get(`/api/tags`); - return response.data; + return checkTags(response.data); }; export const useGetTags = () => { diff --git a/frontend/src/api/tags/typeChecker.ts b/frontend/src/api/tags/typeChecker.ts new file mode 100644 index 000000000..5c41be11e --- /dev/null +++ b/frontend/src/api/tags/typeChecker.ts @@ -0,0 +1,54 @@ +import { AxiosError } from 'axios'; + +import { + arrayOfAll, + checkType, + hasOwnProperties, + hasOwnProperty, + isArray, + isCategoryName, + isNumber, + isObject, + isString, +} from '@utils'; + +import type { Tag } from '@custom-types'; + +import { type ApiTags } from '@api/tags'; + +type TagKeys = keyof Tag; + +const arrayOfAllTagKeys = arrayOfAll(); + +export const checkTag = (data: unknown): Tag => { + if (!isObject(data)) throw new AxiosError(`Tag does not have correct type: object`); + + const keys = arrayOfAllTagKeys(['id', 'name', 'description', 'category']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Tag does not have some properties'); + + if (!isObject(data.category)) throw new AxiosError(`Tag category does not have correct type: object`); + + const categoryKeys: Array = ['id', 'name']; + if (!hasOwnProperties(data.category, categoryKeys)) + throw new AxiosError('Tag category does not have some properties'); + + return { + id: checkType(data.id, isNumber), + name: checkType(data.name, isString), + description: checkType(data.description, isString), + category: { + id: checkType(data.category.id, isNumber), + name: checkType(data.category.name, isCategoryName), + }, + }; +}; + +export const checkTags = (data: unknown): ApiTags['get']['responseData'] => { + if (!isObject(data)) throw new AxiosError(`Tags does not have correct type: object`); + + if (!hasOwnProperty(data, 'tags')) throw new AxiosError('Tags does not have some properties'); + + return { + tags: checkType(data.tags, isArray).map(tag => checkTag(tag)), + }; +}; diff --git a/frontend/public/open-graph-image.png b/frontend/src/assets/images/moamoa-site-image.png similarity index 100% rename from frontend/public/open-graph-image.png rename to frontend/src/assets/images/moamoa-site-image.png diff --git a/frontend/src/components/multi-tag-select/MultiTagSelect.tsx b/frontend/src/components/multi-tag-select/MultiTagSelect.tsx index 030ec493e..db9e61593 100644 --- a/frontend/src/components/multi-tag-select/MultiTagSelect.tsx +++ b/frontend/src/components/multi-tag-select/MultiTagSelect.tsx @@ -1,7 +1,6 @@ import { forwardRef, useEffect, useRef, useState } from 'react'; -import isFunction from '@utils/isFunction'; -import isObject from '@utils/isObject'; +import { isFunction, isObject } from '@utils'; import Center from '@components/center/Center'; import DownArrowIcon from '@components/icons/down-arrow-icon/DownArrowIcon'; diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 3de4f0024..6df9c1d76 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -149,3 +149,26 @@ export const LINK_DESCRIPTION_LENGTH = { }; export const COMMA = ','; + +export const RECRUITMENT_STATUS = { + START: 'RECRUITMENT_START', + END: 'RECRUITMENT_END', +} as const; + +export const USER_ROLE = { + OWNER: 'OWNER', + MEMBER: 'MEMBER', + NON_MEMBER: 'NON_MEMBER', +} as const; + +export const CATEGORY_NAME = { + GENERATION: 'generation', + AREA: 'area', + SUBJECT: 'subject', +} as const; + +export const STUDY_STATUS = { + PREPARE: 'PREPARE', + IN_PROGRESS: 'IN_PROGRESS', + DONE: 'DONE', +} as const; diff --git a/frontend/src/custom-types/index.ts b/frontend/src/custom-types/index.ts index 96eaf407a..12f2f18fb 100644 --- a/frontend/src/custom-types/index.ts +++ b/frontend/src/custom-types/index.ts @@ -1,3 +1,5 @@ +import { CATEGORY_NAME, RECRUITMENT_STATUS, STUDY_STATUS, USER_ROLE } from '@constants'; + export type EmptyObject = Record; export type MakeOptional = Omit & Partial>; @@ -16,7 +18,7 @@ export type MM = `0${oneToNine}` | `1${0 | 1 | 2}`; export type YYYY = `20${d}${d}`; export type DateYMD = `${YYYY}-${MM}-${DD}`; -export type RecruitmentStatus = 'RECRUITMENT_START' | 'RECRUITMENT_END'; +export type RecruitmentStatus = typeof RECRUITMENT_STATUS[keyof typeof RECRUITMENT_STATUS]; export type StudyId = number; export type TagId = number; @@ -28,15 +30,6 @@ export type Page = number; export type Size = number; export type ArticleId = number; -export type SitePage = 'home' | 'studyroom'; - -export type Owner = { - id: MemberId; - username: string; - imageUrl: string; - profileUrl: string; -}; - export type Member = { id: MemberId; username: string; @@ -44,7 +37,9 @@ export type Member = { profileUrl: string; }; -export type CategoryName = 'generation' | 'area' | 'subject'; +export type Owner = Member; + +export type CategoryName = typeof CATEGORY_NAME[keyof typeof CATEGORY_NAME]; export type Tag = { id: TagId; @@ -87,7 +82,7 @@ export type StudyReview = { content: string; }; -export type StudyStatus = 'PREPARE' | 'IN_PROGRESS' | 'DONE'; +export type StudyStatus = typeof STUDY_STATUS[keyof typeof STUDY_STATUS]; export type MyStudy = Pick & { studyStatus: StudyStatus; @@ -95,7 +90,7 @@ export type MyStudy = Pick String(study.id) === studyId); + const studies = studiesJSON.studies; + const study = studies.find(study => String(study.id) === studyId); + if (!study) return res(ctx.status(404), ctx.json({ message: '해당하는 스터디 없음' })); return res(ctx.status(200), ctx.json(study)); }), rest.post('/api/studies', (req, res, ctx) => { @@ -20,6 +24,7 @@ const detailStudyHandlers = [ const { studies } = studiesJSON; + // TODO: json 파일의 타입을 지정할 순 없을까? studiesJSON.studies = [ { id: 1000001, @@ -27,11 +32,11 @@ const detailStudyHandlers = [ title, description, excerpt, - endDate: endDate ?? '', - enrollmentEndDate: enrollmentEndDate ?? '', + endDate: endDate ?? null, + enrollmentEndDate: enrollmentEndDate ?? null, startDate, - maxMemberCount: maxMemberCount ?? 100, - recruitmentStatus: 'OPEN', + maxMemberCount: maxMemberCount ?? null, + recruitmentStatus: RECRUITMENT_STATUS.START, createdDate: '2022-08-18', currentMemberCount: 1, owner: user, diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers/handlers.ts similarity index 75% rename from frontend/src/mocks/handlers.ts rename to frontend/src/mocks/handlers/handlers.ts index 8afdcea7a..a9b511dbd 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers/handlers.ts @@ -1,15 +1,15 @@ import { rest } from 'msw'; -import { communityHandlers } from '@mocks/communityHandler'; -import detailStudyHandlers from '@mocks/detailStudyHandlers'; -import { linkHandlers } from '@mocks/linkHandlers'; -import { memberHandlers } from '@mocks/memberHandlers'; -import { myHandlers } from '@mocks/myHandlers'; -import { noticeHandlers } from '@mocks/noticeHandler'; -import { reviewHandlers } from '@mocks/reviewHandler'; +import { communityHandlers } from '@mocks/handlers/communityHandler'; +import detailStudyHandlers from '@mocks/handlers/detailStudyHandlers'; +import { linkHandlers } from '@mocks/handlers/linkHandlers'; +import { memberHandlers } from '@mocks/handlers/memberHandlers'; +import { myHandlers } from '@mocks/handlers/myHandlers'; +import { noticeHandlers } from '@mocks/handlers/noticeHandler'; +import { reviewHandlers } from '@mocks/handlers/reviewHandler'; +import { tagHandlers } from '@mocks/handlers/tagHandlers'; +import { tokenHandlers } from '@mocks/handlers/tokenHandlers'; import studyJSON from '@mocks/studies.json'; -import { tagHandlers } from '@mocks/tagHandlers'; -import { tokenHandlers } from '@mocks/tokenHandlers'; export const handlers = [ rest.get('/api/studies/search', (req, res, ctx) => { diff --git a/frontend/src/mocks/linkHandlers.ts b/frontend/src/mocks/handlers/linkHandlers.ts similarity index 98% rename from frontend/src/mocks/linkHandlers.ts rename to frontend/src/mocks/handlers/linkHandlers.ts index 74edb951b..02962747e 100644 --- a/frontend/src/mocks/linkHandlers.ts +++ b/frontend/src/mocks/handlers/linkHandlers.ts @@ -1,7 +1,7 @@ import { rest } from 'msw'; +import { user } from '@mocks/handlers/memberHandlers'; import linkJson from '@mocks/links.json'; -import { user } from '@mocks/memberHandlers'; import { ApiLink } from '@api/link'; diff --git a/frontend/src/mocks/memberHandlers.ts b/frontend/src/mocks/handlers/memberHandlers.ts similarity index 100% rename from frontend/src/mocks/memberHandlers.ts rename to frontend/src/mocks/handlers/memberHandlers.ts diff --git a/frontend/src/mocks/myHandlers.ts b/frontend/src/mocks/handlers/myHandlers.ts similarity index 100% rename from frontend/src/mocks/myHandlers.ts rename to frontend/src/mocks/handlers/myHandlers.ts diff --git a/frontend/src/mocks/noticeHandler.ts b/frontend/src/mocks/handlers/noticeHandler.ts similarity index 98% rename from frontend/src/mocks/noticeHandler.ts rename to frontend/src/mocks/handlers/noticeHandler.ts index 9e9c732d5..fc86eeda2 100644 --- a/frontend/src/mocks/noticeHandler.ts +++ b/frontend/src/mocks/handlers/noticeHandler.ts @@ -1,6 +1,6 @@ import { rest } from 'msw'; -import { user } from '@mocks/memberHandlers'; +import { user } from '@mocks/handlers/memberHandlers'; import noticeArticlesJSON from '@mocks/notice-articles.json'; import { type ApiNoticeArticle } from '@api/notice'; diff --git a/frontend/src/mocks/reviewHandler.ts b/frontend/src/mocks/handlers/reviewHandler.ts similarity index 97% rename from frontend/src/mocks/reviewHandler.ts rename to frontend/src/mocks/handlers/reviewHandler.ts index 6444dfb22..dc6374bdd 100644 --- a/frontend/src/mocks/reviewHandler.ts +++ b/frontend/src/mocks/handlers/reviewHandler.ts @@ -1,6 +1,6 @@ import { rest } from 'msw'; -import { user } from '@mocks/memberHandlers'; +import { user } from '@mocks/handlers/memberHandlers'; import reviewJSON from '@mocks/reviews.json'; import { type ApiReview } from '@api/review'; diff --git a/frontend/src/mocks/tagHandlers.ts b/frontend/src/mocks/handlers/tagHandlers.ts similarity index 100% rename from frontend/src/mocks/tagHandlers.ts rename to frontend/src/mocks/handlers/tagHandlers.ts diff --git a/frontend/src/mocks/tokenHandlers.ts b/frontend/src/mocks/handlers/tokenHandlers.ts similarity index 100% rename from frontend/src/mocks/tokenHandlers.ts rename to frontend/src/mocks/handlers/tokenHandlers.ts diff --git a/frontend/src/mocks/reviews.json b/frontend/src/mocks/reviews.json index 258734491..e50950422 100644 --- a/frontend/src/mocks/reviews.json +++ b/frontend/src/mocks/reviews.json @@ -9,7 +9,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-26", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "우리는 봄바람을 목숨이 어디 같으며, 것은 뜨거운지라, 보배를 봄바람이다. 심장은 앞이 기쁘며, 설레는 것이다. 얼음에 갑 그것은 힘있다. 피가 소금이라 남는 길지 않는 그리하였는가? 피어나기 풀이 가슴이 뜨거운지라, 힘차게 무엇을 인생에 때문이다. 보내는 하였으며, 이상은 사랑의 얼마나 이상, 보이는 가는 별과 이것이다. 하는 주는 있으며, 하는 위하여 원대하고, 쓸쓸한 아름다우냐? 피고 내려온 위하여, 그들의 못하다 몸이 뭇 봄바람이다. 청춘의 끝까지 귀는 일월과 심장은 그러므로 어디 듣는다. 인간이 이상, 밥을 미인을 것이다. 원질이 소담스러운 이상의 이상, 얼마나 불어 싸인 뿐이다." }, { @@ -21,7 +21,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-27", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "평화스러운 이상을 풀이 그들에게 청춘을 그들의 같이, 발휘하기 힘있다. 끓는 청춘을 꽃 밥을 이것이다. 길지 가치를 기관과 듣기만 살았으며, 무한한 것이다. 유소년에게서 같은 가슴이 타오르고 뛰노는 것이다. 하여도 것이다.보라, 넣는 것은 설레는 우리는 청춘 그들은 그들의 것이다. 보이는 이는 피고, 보라." }, { @@ -33,7 +33,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-31", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "과실이 유소년에게서 못할 곧 무엇을 구하기 풍부하게 소담스러운 바이며, 있는가? 밝은 발휘하기 위하여 듣기만 불어 아니다. 온갖 보는 전인 싹이 별과 같은 운다. 가치를 것이 그림자는 보라. 얼음이 너의 사는가 철환하였는가? 뛰노는 열락의 새 바로 것이다. 공자는 따뜻한 피어나는 오직 오아이스도 것이다." }, { @@ -45,7 +45,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-16", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "못할 우는 품으며, 낙원을 할지라도 안고, 용감하고 따뜻한 못하다 봄바람이다. 생생하며, 무엇을 것이 있는 별과 천자만홍이 불어 이상 그리하였는가? 산야에 우리 무한한 설산에서 전인 부패를 뜨고, 황금시대다. 그들은 붙잡아 이것이야말로 가치를 약동하다. 설레는 열락의 일월과 눈이 끝에 이상 같으며, 너의 군영과 봄바람이다. 구하기 그것은 찬미를 밥을 보내는 봄바람이다." }, { @@ -57,7 +57,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-29", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "이상, 없으면 동산에는 것이다. 그들은 주며, 인생에 불어 위하여서 품에 이 속에서 보이는 약동하다. 같은 불러 바로 위하여, 것이다." }, { @@ -69,7 +69,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-01", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "위하여 이상의 별과 창공에 설레는 대중을 그것은 그들의 사막이다. 청춘의 따뜻한 온갖 위하여, 피가 싸인 우리의 힘차게 봄바람이다." }, { @@ -81,7 +81,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-26", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "물방아 방황하였으며, 않는 오직 이것을 부패뿐이다. 그들에게 몸이 관현악이며, 기쁘며, 오직 위하여서. 생생하며, 생명을 무엇을 같은 따뜻한 청춘 뜨고, 커다란 눈에 봄바람이다. 있는 그들에게 주는 피가 있으며, 때에, 기관과 것이다." }, { @@ -93,7 +93,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-25", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "돋고, 청춘의 구하지 현저하게 할지니, 피가 힘있다. 이성은 반짝이는 살았으며, 사막이다." }, { @@ -105,7 +105,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-24", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "그러므로 불어 청춘의 커다란 것이다.보라, 거선의 무엇을 맺어, 봄바람이다. 풍부하게 수 노래하며 품으며, 창공에 인간의 사막이다. 그들의 풍부하게 바이며, 옷을 철환하였는가?" }, { @@ -117,7 +117,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-27", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "용감하고 목숨을 사람은 때에, 이상은 힘차게 칼이다. 끝까지 반짝이는 능히 뿐이다." }, { @@ -129,7 +129,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-01", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "풀이 힘차게 청춘이 인생에 약동하다. 하여도 없으면, 뜨거운지라, 작고 그들은 끝까지 남는 커다란 얼마나 힘있다. 기관과 이상을 사랑의 눈에 이것을 남는 천자만홍이 놀이 밝은 쓸쓸하랴? 청춘의 든 타오르고 가치를 미인을 구할 있음으로써 찬미를 작고 칼이다." }, { @@ -141,7 +141,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-04", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "열매를 인도하겠다는 내는 있으랴? 역사를 것은 기쁘며, 앞이 있음으로써 천고에 듣기만 아름다우냐? 이상은 위하여서 곳이 오직 위하여, 원질이 그러므로 듣는다." }, { @@ -153,7 +153,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-20", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "뼈 창공에 앞이 있는가? 피부가 우리 맺어, 기관과 현저하게 없으면, 곳이 낙원을 것은 듣는다." }, { @@ -165,7 +165,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-19", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "이상 실현에 노년에게서 없으면 청춘을 남는 피어나기 것이다. 청춘을 얼마나 그들을 새가 석가는 그러므로 두손을 이상은 이것이다. 찾아다녀도, 창공에 착목한는 교향악이다. 천지는 것은 이상이 별과 위하여서." }, { @@ -177,7 +177,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-22", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "오아이스도 이 자신과 같은 그들의 인생의 청춘 못하다 교향악이다. 황금시대의 얼마나 두기 봄바람을 청춘의 작고 풀밭에 운다. 얼음과 뜨거운지라, 영락과 인간의 위하여 시들어 사막이다. 간에 날카로우나 사는가 그것은 쓸쓸하랴?" }, { @@ -189,7 +189,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-11", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "못할 든 동력은 피고, 인간의 이것이다. 찬미를 가치를 소금이라 길지 몸이 봄바람이다." }, { @@ -201,7 +201,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-03", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "오아이스도 같이, 행복스럽고 희망의 하였으며, 만물은 힘있다. 구하지 있으며, 것은 긴지라 이상 꽃이 것이다." }, { @@ -213,7 +213,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-30", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "길지 무엇을 자신과 노래하며 앞이 그러므로 품었기 것이다. 끓는 아니더면, 인생의 그림자는 붙잡아 작고 원질이 인생에 쓸쓸하랴? 반짝이는 끓는 인간은 싹이 청춘에서만 교향악이다." }, { @@ -225,7 +225,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-25", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "인간의 속에 꾸며 것이다. 속에 거선의 힘차게 인류의 생의 힘있다. 타오르고 원대하고, 풍부하게 얼마나 때에, 사막이다." }, { @@ -237,7 +237,7 @@ "profileUrl": "https://github.com/airman5573" }, "createdDate": "2022-07-07", - "lastModifiedDate": "", + "lastModifiedDate": "2022-09-10", "content": "보이는 풀이 보이는 따뜻한 방지하는 그러므로 이성은 같지 속에서 있다. 불어 청춘이 용기가 실현에 있는 굳세게 아니다." } ], diff --git a/frontend/src/mocks/studies.json b/frontend/src/mocks/studies.json index baf533896..c90406bc4 100644 --- a/frontend/src/mocks/studies.json +++ b/frontend/src/mocks/studies.json @@ -10,9 +10,9 @@ "currentMemberCount": 26, "maxMemberCount": 40, "createdDate": "2022-07-21", - "enrollmentEndDate": "2022-07-10", + "enrollmentEndDate": null, "startDate": "2022-07-24", - "endDate": "2022-08-10", + "endDate": null, "owner": { "id": 1, "username": "jaejae-yoo", @@ -286,7 +286,7 @@ "currentMemberCount": 25, "maxMemberCount": 33, "createdDate": "2022-07-21", - "enrollmentEndDate": "2022-07-19", + "enrollmentEndDate": null, "startDate": "2022-07-01", "endDate": "2022-08-17", "owner": { @@ -558,7 +558,7 @@ "recruitmentStatus": "RECRUITMENT_END", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 39, - "maxMemberCount": 40, + "maxMemberCount": null, "createdDate": "2022-07-21", "enrollmentEndDate": "2022-07-24", "startDate": "2022-07-14", @@ -1498,7 +1498,7 @@ "recruitmentStatus": "RECRUITMENT_END", "description": "# Prettier plugin sort imports\n\nA prettier plugin to sort import declarations by provided Regular Expression order.\n\n**Note: If you are migrating from v2.x.x to v3.x.x, [Please Read Migration Guidelines](./docs/MIGRATION.md)**\n\n### Input\n\n```javascript\nimport React, {\n FC,\n useEffect,\n useRef,\n ChangeEvent,\n KeyboardEvent,\n} from \"react\";\nimport { logger } from \"@core/logger\";\nimport { reduce, debounce } from \"lodash\";\nimport { Message } from \"../Message\";\nimport { createServer } from \"@server/node\";\nimport { Alert } from \"@ui/Alert\";\nimport { repeat, filter, add } from \"../utils\";\nimport { initializeApp } from \"@core/app\";\nimport { Popup } from \"@ui/Popup\";\nimport { createConnection } from \"@server/database\";\n```\n\n### Output\n\n```javascript\nimport { debounce, reduce } from \"lodash\";\nimport React, {\n ChangeEvent,\n FC,\n KeyboardEvent,\n useEffect,\n useRef,\n} from \"react\";\n\nimport { createConnection } from \"@server/database\";\nimport { createServer } from \"@server/node\";\n\nimport { initializeApp } from \"@core/app\";\nimport { logger } from \"@core/logger\";\n\nimport { Alert } from \"@ui/Alert\";\nimport { Popup } from \"@ui/Popup\";\n\nimport { Message } from \"../Message\";\nimport { add, filter, repeat } from \"../utils\";\n```\n", "currentMemberCount": 29, - "maxMemberCount": 29, + "maxMemberCount": null, "createdDate": "2022-07-21", "enrollmentEndDate": "2022-07-27", "startDate": "2022-07-13", diff --git a/frontend/src/pages/create-study-page/components/category/Category.tsx b/frontend/src/pages/create-study-page/components/category/Category.tsx index c1f083d82..1f5f74abf 100644 --- a/frontend/src/pages/create-study-page/components/category/Category.tsx +++ b/frontend/src/pages/create-study-page/components/category/Category.tsx @@ -1,3 +1,5 @@ +import { CATEGORY_NAME } from '@constants'; + import tw from '@utils/tw'; import type { StudyDetail, Tag } from '@custom-types'; @@ -17,7 +19,7 @@ export type CategoryProps = { originalAreas?: StudyDetail['tags']; }; -const GENERATION = 'generation'; +const GENERATION = CATEGORY_NAME.GENERATION; const AREA_FE = 'area-fe'; const AREA_BE = 'area-be'; diff --git a/frontend/src/pages/detail-page/components/head/Head.stories.tsx b/frontend/src/pages/detail-page/components/head/Head.stories.tsx index 8e25631fe..22ec2e7ff 100644 --- a/frontend/src/pages/detail-page/components/head/Head.stories.tsx +++ b/frontend/src/pages/detail-page/components/head/Head.stories.tsx @@ -1,5 +1,7 @@ import type { Story } from '@storybook/react'; +import { RECRUITMENT_STATUS } from '@constants'; + import Head from '@detail-page/components/head/Head'; import type { HeadProps } from '@detail-page/components/head/Head'; @@ -17,7 +19,7 @@ const Template: Story = props => ( export const Default = Template.bind({}); Default.args = { title: '2022-모아모아', - recruitmentStatus: 'RECRUITMENT_START', + recruitmentStatus: RECRUITMENT_STATUS.START, startDate: '2022-07-18', endDate: '2022-07-18', excerpt: '모아모아 최고~~', diff --git a/frontend/src/pages/detail-page/components/head/Head.tsx b/frontend/src/pages/detail-page/components/head/Head.tsx index 2fbc0111f..3b79f5169 100644 --- a/frontend/src/pages/detail-page/components/head/Head.tsx +++ b/frontend/src/pages/detail-page/components/head/Head.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; -import { PATH } from '@constants'; +import { PATH, RECRUITMENT_STATUS } from '@constants'; import { changeDateSeperator } from '@utils'; @@ -35,7 +35,7 @@ const Head: React.FC = ({ {title} - + {isOwner && ( diff --git a/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx b/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx index b0dd9b875..e9cb3a803 100644 --- a/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; -import { PATH } from '@constants'; +import { PATH, RECRUITMENT_STATUS, USER_ROLE } from '@constants'; import { yyyymmddTommdd } from '@utils'; import tw from '@utils/tw'; @@ -33,10 +33,10 @@ const StudyFloatBox: React.FC = ({ recruitmentStatus, onRegisterButtonClick: handleRegisterButtonClick, }) => { - const isOpen = recruitmentStatus === 'RECRUITMENT_START'; + const isOpen = recruitmentStatus === RECRUITMENT_STATUS.START; const renderEnrollmentEndDateContent = () => { - if (userRole === 'MEMBER' || userRole === 'OWNER') { + if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { return 이미 가입한 스터디입니다; } @@ -57,7 +57,7 @@ const StudyFloatBox: React.FC = ({ }; const renderButton = () => { - if (userRole === 'MEMBER' || userRole === 'OWNER') { + if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { return ( diff --git a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx index 4ad31b9eb..b122733af 100644 --- a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; -import { PATH } from '@constants'; +import { PATH, RECRUITMENT_STATUS, USER_ROLE } from '@constants'; import { yyyymmddTommdd } from '@utils'; import tw from '@utils/tw'; @@ -31,10 +31,10 @@ const StudyWideFloatBox: React.FC = ({ recruitmentStatus, onRegisterButtonClick: handleRegisterButtonClick, }) => { - const isOpen = recruitmentStatus === 'RECRUITMENT_START'; + const isOpen = recruitmentStatus === RECRUITMENT_STATUS.START; const renderEnrollmentEndDateContent = () => { - if (userRole === 'MEMBER' || userRole === 'OWNER') { + if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { return 이미 가입한 스터디입니다; } @@ -54,7 +54,7 @@ const StudyWideFloatBox: React.FC = ({ }; const renderButton = () => { - if (userRole === 'MEMBER' || userRole === 'OWNER') { + if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { return ( diff --git a/frontend/src/pages/edit-study-page/EditStudyPage.tsx b/frontend/src/pages/edit-study-page/EditStudyPage.tsx index fae670571..462018ad4 100644 --- a/frontend/src/pages/edit-study-page/EditStudyPage.tsx +++ b/frontend/src/pages/edit-study-page/EditStudyPage.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { PATH } from '@constants'; +import { CATEGORY_NAME, PATH } from '@constants'; import { FormProvider } from '@hooks/useForm'; @@ -36,9 +36,9 @@ const EditStudyPage: React.FC = () => { if (isError || !isSuccess) return
에러가 발생했습니다 :(
; // TODO: 반복문 -> useMemo를 사용하는 건 어떨까? - const originalGeneration = data.tags.find(tag => tag.category.name === 'generation'); - const originalAreas = data.tags.filter(tag => tag.category.name === 'area'); - const originalSubjects = data.tags.filter(tag => tag.category.name === 'subject'); + const originalGeneration = data.tags.find(tag => tag.category.name === CATEGORY_NAME.GENERATION); + const originalAreas = data.tags.filter(tag => tag.category.name === CATEGORY_NAME.AREA); + const originalSubjects = data.tags.filter(tag => tag.category.name === CATEGORY_NAME.SUBJECT); return ( diff --git a/frontend/src/pages/main-page/MainPage.tsx b/frontend/src/pages/main-page/MainPage.tsx index e1ffecc6f..16d7f0020 100644 --- a/frontend/src/pages/main-page/MainPage.tsx +++ b/frontend/src/pages/main-page/MainPage.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; -import { PATH } from '@constants'; +import { PATH, RECRUITMENT_STATUS } from '@constants'; import type { Study } from '@custom-types'; @@ -42,7 +42,7 @@ const MainPage: React.FC = () => { title={study.title} excerpt={study.excerpt} tags={study.tags} - isOpen={study.recruitmentStatus === 'RECRUITMENT_START'} + isOpen={study.recruitmentStatus === RECRUITMENT_STATUS.START} /> diff --git a/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts index a333b4344..47f2a4341 100644 --- a/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts +++ b/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts @@ -1,5 +1,7 @@ import { useMemo } from 'react'; +import { STUDY_STATUS } from '@constants'; + import type { MyStudy, StudyStatus } from '@custom-types'; import { useGetMyStudies } from '@api/my-studies'; @@ -14,9 +16,9 @@ export const useMyStudyPage = () => { const filteredStudies: Record> = useMemo(() => { const studies = myStudyQueryResult.data?.studies ?? []; return { - prepare: filterStudiesByStatus(studies, 'PREPARE'), - inProgress: filterStudiesByStatus(studies, 'IN_PROGRESS'), - done: filterStudiesByStatus(studies, 'DONE'), + prepare: filterStudiesByStatus(studies, STUDY_STATUS.PREPARE), + inProgress: filterStudiesByStatus(studies, STUDY_STATUS.IN_PROGRESS), + done: filterStudiesByStatus(studies, STUDY_STATUS.DONE), }; }, [myStudyQueryResult.data]); diff --git a/frontend/src/pages/study-room-page/StudyRoomPage.tsx b/frontend/src/pages/study-room-page/StudyRoomPage.tsx index c380c7bf0..3eefdb37a 100644 --- a/frontend/src/pages/study-room-page/StudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/StudyRoomPage.tsx @@ -1,6 +1,6 @@ import { Navigate, Outlet } from 'react-router-dom'; -import { PATH } from '@constants'; +import { PATH, USER_ROLE } from '@constants'; import tw from '@utils/tw'; @@ -14,7 +14,7 @@ const StudyRoomPage: React.FC = () => { const { tabs, activeTabId, userRoleQueryResult, handleTabButtonClick } = useStudyRoomPage(); const { data, isError, isSuccess } = userRoleQueryResult; - if (isSuccess && data.role === 'NON_MEMBER') { + if (isSuccess && data.role === USER_ROLE.NON_MEMBER) { alert('잘못된 접근입니다.'); return ; } diff --git a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-item/LinkItem.tsx b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-item/LinkItem.tsx index 0a45ca97b..46ed9ea4e 100644 --- a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-item/LinkItem.tsx +++ b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-item/LinkItem.tsx @@ -1,3 +1,5 @@ +import SiteImage from '@assets/images/moamoa-site-image.png'; + import tw from '@utils/tw'; import type { Link, StudyId } from '@custom-types'; @@ -45,10 +47,10 @@ const LinkItem: React.FC = ({ studyId, id: linkId, linkUrl, autho const { data, isError, isSuccess, isFetching } = linkPreviewQueryResult; const errorPreviewResult: ApiLinkPreview['get']['responseData'] = { - title: '%Error%', - description: '링크 불러오기에 실패했습니다 :(', - imageUrl: null, - domainName: null, + title: linkUrl, + description: linkUrl, + imageUrl: SiteImage, + domainName: linkUrl, }; if (isFetching) return
로딩중...
; diff --git a/frontend/src/utils/arrayOfAll.ts b/frontend/src/utils/arrayOfAll.ts new file mode 100644 index 000000000..26163d891 --- /dev/null +++ b/frontend/src/utils/arrayOfAll.ts @@ -0,0 +1,13 @@ +/* How to use: + * type BreakPoints = 'xs' | 'sm' | 'md'; + * const arrayOfAllBreakPoints = arrayOfAll(); + * const wrongBreakPoints = arrayOfAllBreakPoints(['xs', 'sm']); // Error + * const breakPoints = arrayOfAllBreakPoints(['xs', 'sm', 'md']); // Ok + */ + +const arrayOfAll = + () => + (array: U & ([T] extends [U[number]] ? unknown : 'Invalid')) => + array; + +export default arrayOfAll; diff --git a/frontend/src/utils/dates.ts b/frontend/src/utils/dates.ts index c1df7b1ae..4cc107564 100644 --- a/frontend/src/utils/dates.ts +++ b/frontend/src/utils/dates.ts @@ -1,9 +1,6 @@ -import { DateYMD } from '@custom-types'; +import { isDateYMD } from '@utils'; -const isDateYMD = (date: string): date is DateYMD => { - const regex = /\d{4}-\d{2}-\d{2}/; - return regex.test(date); -}; +import { DateYMD } from '@custom-types'; export const getToday = (): DateYMD => { const koKRDate = new Date().toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }); diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 251e4de90..2761e2e53 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,5 +1,7 @@ export { default as getRandomInt } from '@utils/getRandomInt'; export { default as noop } from '@utils/noop'; -export { mqBetween, mqDown, mqUp } from '@utils/media-query'; -export { changeDateSeperator, getNextYear, getToday, yyyymmddTommdd } from '@utils/dates'; -export { isObject, isString } from '@utils/type-checks'; +export * from '@utils/media-query'; +export * from '@utils/dates'; +export * from '@utils/typeChecker'; +export { default as checkType } from '@utils/typeChecker'; +export { default as arrayOfAll } from '@utils/arrayOfAll'; diff --git a/frontend/src/utils/isFunction.ts b/frontend/src/utils/isFunction.ts deleted file mode 100644 index 75fe8674e..000000000 --- a/frontend/src/utils/isFunction.ts +++ /dev/null @@ -1,7 +0,0 @@ -type TFunction = (...args: any[]) => any; - -const isFunction = (val: unknown): val is TFunction => { - return typeof val === 'function'; -}; - -export default isFunction; diff --git a/frontend/src/utils/isNullOrUndefined.ts b/frontend/src/utils/isNullOrUndefined.ts deleted file mode 100644 index 48c0c3f99..000000000 --- a/frontend/src/utils/isNullOrUndefined.ts +++ /dev/null @@ -1,3 +0,0 @@ -const isNullOrUndefined = (value: unknown): value is null | undefined => value === undefined || value === null; - -export default isNullOrUndefined; diff --git a/frontend/src/utils/isObject.ts b/frontend/src/utils/isObject.ts deleted file mode 100644 index c280bba63..000000000 --- a/frontend/src/utils/isObject.ts +++ /dev/null @@ -1,6 +0,0 @@ -import isNullOrUndefined from '@utils/isNullOrUndefined'; - -export const isObjectType = (value: unknown) => typeof value === 'object'; - -export default (value: unknown): value is T => - !isNullOrUndefined(value) && !Array.isArray(value) && isObjectType(value); diff --git a/frontend/src/utils/type-checks.ts b/frontend/src/utils/type-checks.ts deleted file mode 100644 index a31c20bbc..000000000 --- a/frontend/src/utils/type-checks.ts +++ /dev/null @@ -1,8 +0,0 @@ -import isNullOrUndefined from '@utils/isNullOrUndefined'; - -const isObjectType = (value: unknown) => typeof value === 'object'; - -export const isObject = (value: unknown): value is T => - !isNullOrUndefined(value) && !Array.isArray(value) && isObjectType(value); - -export const isString = (value: unknown): value is string => typeof value === 'string'; diff --git a/frontend/src/utils/typeChecker.ts b/frontend/src/utils/typeChecker.ts new file mode 100644 index 000000000..85a1766e4 --- /dev/null +++ b/frontend/src/utils/typeChecker.ts @@ -0,0 +1,72 @@ +import { CATEGORY_NAME, RECRUITMENT_STATUS, STUDY_STATUS, USER_ROLE } from '@constants'; + +import type { CategoryName, DateYMD, RecruitmentStatus, StudyStatus, UserRole } from '@custom-types'; + +export const isNull = (value: unknown): value is null => value === null; + +export const isNullOrUndefined = (value: unknown): value is null | undefined => value === undefined || value === null; + +export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean'; + +export const isString = (value: unknown): value is string => typeof value === 'string'; + +export const isNumber = (value: unknown): value is number => typeof value === 'number'; + +export const isFunction = (val: unknown): val is (...args: any) => any => typeof val === 'function'; + +export const isObject = (value: unknown): value is T => + typeof value === 'object' && !isNullOrUndefined(value) && !Array.isArray(value); + +export const isArray = (value: unknown): value is Array => Array.isArray(value); + +export const isDateYMD = (value: unknown): value is DateYMD => { + if (!isString(value)) return false; + const regex = /\d{4}-\d{2}-\d{2}/; + return regex.test(value); +}; + +export const isUserRole = (value: unknown): value is UserRole => + isString(value) && Object.values(USER_ROLE).some(role => role === value); + +export const isStudyStatus = (value: unknown): value is StudyStatus => + isString(value) && Object.values(STUDY_STATUS).some(status => status === value); + +export const isRecruitmentStatus = (value: unknown): value is RecruitmentStatus => + isString(value) && Object.values(RECRUITMENT_STATUS).some(status => status === value); + +export const isCategoryName = (value: unknown): value is CategoryName => + isString(value) && Object.values(CATEGORY_NAME).some(name => name === value); + +export const hasOwnProperty = ( + obj: X, + prop: Y, +): obj is X & Record => Object.hasOwn(obj, prop); + +export const hasOwnProperties = ( + obj: X, + props: Array, +): obj is X & Record => props.every(prop => Object.hasOwn(obj, prop)); + +type isTypeFn = (value: unknown) => value is T; + +function checkType(value: unknown, isType: isTypeFn, isOptional: true): T | undefined; +function checkType(value: unknown, isType: isTypeFn, isOptional?: false): T; +function checkType(value: unknown, isType: isTypeFn, isOptional?: boolean): T | undefined { + if (isOptional) { + if (!isType(value) && !isNull(value)) { + console.error(`${isType} ${value} does not have correct type`); + throw new Error(`${value} does not have correct type`); + } + + return value ?? undefined; + } + + if (!isType(value)) { + console.error(`${isType} ${value} does not have correct type`); + throw new Error(`${value} does not have correct type`); + } + + return value; +} + +export default checkType; From 9c0fce421acfc90ee6e7a134b5fc4668c3c2c45f Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Tue, 11 Oct 2022 11:22:03 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9E=99=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20secret=20value=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ae7ee18db..afbc123b0 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -31,6 +31,9 @@ jobs: client-secret: ${{ secrets.CLIENT_SECRET }} jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + SLACK_USERS : ${(SLACK_USERS}} + SLACK_SAND_MESSAGE : ${{secrets.SLACK_SAND_MESSAGE}} + SLACK_AUTHORIZATION : ${{secrets.SLACK_AUTHORIZATION}} steps: - name: Checkout uses: actions/checkout@v3 From 58d8eb0c1bc4b60fe2579b0c5decdf2afd3f3c9e Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Tue, 11 Oct 2022 11:23:10 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9E=99=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20secret=20value=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-backend-dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy-backend-dev.yml b/.github/workflows/deploy-backend-dev.yml index cab362827..f56560b4c 100644 --- a/.github/workflows/deploy-backend-dev.yml +++ b/.github/workflows/deploy-backend-dev.yml @@ -40,6 +40,9 @@ jobs: client-secret: ${{ secrets.CLIENT_SECRET }} jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + SLACK_USERS : ${(secrets.SLACK_USERS}} + SLACK_SAND_MESSAGE : ${{secrets.SLACK_SAND_MESSAGE}} + SLACK_AUTHORIZATION : ${{secrets.SLACK_AUTHORIZATION}} run: ./gradlew test -Dmoamoa.allow-origins='*' -Doauth2.github.client-id=${{ env.client-id }} -Doauth2.github.client-secret=${{ env.client-secret }} -Dsecurity.jwt.token.secret-key=${{ env.jwt-secret-key }} -Dsecurity.jwt.token.expire-length=${{ env.jwt-expire-length }} - name: SonarQube From 0540c58369bda134125af9f12918246b847edaea Mon Sep 17 00:00:00 2001 From: jaeseo yoo Date: Tue, 11 Oct 2022 11:23:47 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9E=99=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20secrets=20value=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index afbc123b0..d14fe8243 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -31,7 +31,7 @@ jobs: client-secret: ${{ secrets.CLIENT_SECRET }} jwt-secret-key: ${{ secrets.JWT_SECRET_KEY }} jwt-expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} - SLACK_USERS : ${(SLACK_USERS}} + SLACK_USERS : ${(secrets.SLACK_USERS}} SLACK_SAND_MESSAGE : ${{secrets.SLACK_SAND_MESSAGE}} SLACK_AUTHORIZATION : ${{secrets.SLACK_AUTHORIZATION}} steps: From 240e02e5209ebd51983797d54a523f1de8f8f45a Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Tue, 11 Oct 2022 20:43:17 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=20api=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main page 스터디 목록 - my study page 스터디 목록 -> 타입 충돌 오류 수정 --- frontend/src/api/my-study/typeChecker.ts | 2 +- frontend/src/api/studies/typeChecker.ts | 52 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/my-study/typeChecker.ts b/frontend/src/api/my-study/typeChecker.ts index 6f97f8f17..7bd827acb 100644 --- a/frontend/src/api/my-study/typeChecker.ts +++ b/frontend/src/api/my-study/typeChecker.ts @@ -47,7 +47,7 @@ export const checkMyStudy = (data: unknown): MyStudy => { id: checkType(data.id, isNumber), title: checkType(data.title, isString), startDate: checkType(data.startDate, isDateYMD), - endDate: checkType(data.endDate, isDateYMD), + endDate: checkType(data.endDate, isDateYMD, true), studyStatus: checkType(data.studyStatus, isStudyStatus), tags: checkType(data.tags, isArray).map(tag => checkMyStudyTag(tag)), owner: checkMember(data.owner), diff --git a/frontend/src/api/studies/typeChecker.ts b/frontend/src/api/studies/typeChecker.ts index 771d4db01..e4fde0726 100644 --- a/frontend/src/api/studies/typeChecker.ts +++ b/frontend/src/api/studies/typeChecker.ts @@ -1,9 +1,57 @@ import { AxiosError } from 'axios'; -import { arrayOfAll, checkType, hasOwnProperties, isArray, isBoolean, isObject } from '@utils'; +import { + arrayOfAll, + checkType, + hasOwnProperties, + isArray, + isBoolean, + isNumber, + isObject, + isRecruitmentStatus, + isString, +} from '@utils'; + +import type { Study, Tag } from '@custom-types'; import { type ApiStudies } from '@api/studies'; -import { checkStudy } from '@api/study/typeChecker'; + +type MainStudyTag = Pick; +type MainStudyTagKeys = keyof MainStudyTag; + +const arrayOfAllMainStudyTagKeys = arrayOfAll(); + +const checkMainStudyTag = (data: unknown): MainStudyTag => { + if (!isObject(data)) throw new AxiosError(`Main-Tag does not have correct type: object`); + + const keys = arrayOfAllMainStudyTagKeys(['id', 'name']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Main-Tag does not have some properties'); + + return { + id: checkType(data.id, isNumber), + name: checkType(data.name, isString), + }; +}; + +type StudyKeys = keyof Study; + +const arrayOfAllStudyKeys = arrayOfAll(); + +const checkStudy = (data: unknown): Study => { + if (!isObject(data)) throw new AxiosError(`Main-Study does not have correct type: object`); + + const keys = arrayOfAllStudyKeys(['id', 'excerpt', 'recruitmentStatus', 'tags', 'thumbnail', 'title']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('Main-Study does not have some properties'); + + return { + id: checkType(data.id, isNumber), + title: checkType(data.title, isString), + excerpt: checkType(data.title, isString), + thumbnail: checkType(data.title, isString), + tags: checkType(data.tags, isArray).map(tag => checkMainStudyTag(tag)), + recruitmentStatus: checkType(data.recruitmentStatus, isRecruitmentStatus), + }; +}; type StudiesKeys = keyof ApiStudies['get']['responseData']; From df37033cc134641954daf4796be1d838a5feaa36 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Wed, 12 Oct 2022 11:06:41 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20notice/community=20article=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/community/index.ts | 2 +- frontend/src/api/community/typeChecker.ts | 24 ++++++++++++++++++++++- frontend/src/api/notice/index.ts | 2 +- frontend/src/api/notice/typeChecker.ts | 24 ++++++++++++++++++++++- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/community/index.ts b/frontend/src/api/community/index.ts index 66ed4d993..ec099b848 100644 --- a/frontend/src/api/community/index.ts +++ b/frontend/src/api/community/index.ts @@ -11,7 +11,7 @@ import { checkCommunityArticle, checkCommunityArticles } from '@api/community/ty export type ApiCommunityArticles = { get: { responseData: { - articles: Array; + articles: Array>; currentPage: number; lastPage: number; totalCount: number; diff --git a/frontend/src/api/community/typeChecker.ts b/frontend/src/api/community/typeChecker.ts index 203e7a14b..b61049c52 100644 --- a/frontend/src/api/community/typeChecker.ts +++ b/frontend/src/api/community/typeChecker.ts @@ -2,6 +2,8 @@ import { AxiosError } from 'axios'; import { arrayOfAll, checkType, hasOwnProperties, isArray, isDateYMD, isNumber, isObject, isString } from '@utils'; +import type { CommunityArticle } from '@custom-types'; + import { type ApiCommunityArticle, type ApiCommunityArticles } from '@api/community'; import { checkMember } from '@api/member/typeChecker'; @@ -25,6 +27,26 @@ export const checkCommunityArticle = (data: unknown): ApiCommunityArticle['get'] }; }; +type Article = Omit; +type ArticleKeys = keyof Article; + +const arrayOfAllArticleKeys = arrayOfAll(); + +const checkArticle = (data: unknown): Article => { + if (!isObject(data)) throw new AxiosError(`CommunityArticles-Article does not have correct type: object`); + + const keys = arrayOfAllArticleKeys(['id', 'author', 'title', 'createdDate', 'lastModifiedDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('CommunityArticles-Article does not have some properties'); + + return { + id: checkType(data.id, isNumber), + author: checkMember(data.author), + title: checkType(data.title, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), + }; +}; + type CommunityArticlesKeys = keyof ApiCommunityArticles['get']['responseData']; const arrayOfAllCommunityArticlesKeys = arrayOfAll(); @@ -36,7 +58,7 @@ export const checkCommunityArticles = (data: unknown): ApiCommunityArticles['get if (!hasOwnProperties(data, keys)) throw new AxiosError('CommunityArticles does not have some properties'); return { - articles: checkType(data.articles, isArray).map(article => checkCommunityArticle(article)), + articles: checkType(data.articles, isArray).map(article => checkArticle(article)), currentPage: checkType(data.currentPage, isNumber) + 1, lastPage: checkType(data.lastPage, isNumber), totalCount: checkType(data.totalCount, isNumber), diff --git a/frontend/src/api/notice/index.ts b/frontend/src/api/notice/index.ts index 36393c9bd..1cd075806 100644 --- a/frontend/src/api/notice/index.ts +++ b/frontend/src/api/notice/index.ts @@ -11,7 +11,7 @@ import { checkNoticeArticle, checkNoticeArticles } from '@api/notice/typeChecker export type ApiNoticeArticles = { get: { responseData: { - articles: Array; + articles: Array>; currentPage: number; lastPage: number; totalCount: number; diff --git a/frontend/src/api/notice/typeChecker.ts b/frontend/src/api/notice/typeChecker.ts index 14b06858f..fbc09bfd8 100644 --- a/frontend/src/api/notice/typeChecker.ts +++ b/frontend/src/api/notice/typeChecker.ts @@ -2,6 +2,8 @@ import { AxiosError } from 'axios'; import { arrayOfAll, checkType, hasOwnProperties, isArray, isDateYMD, isNumber, isObject, isString } from '@utils'; +import type { NoticeArticle } from '@custom-types'; + import { checkMember } from '@api/member/typeChecker'; import { type ApiNoticeArticle, type ApiNoticeArticles } from '@api/notice'; @@ -25,6 +27,26 @@ export const checkNoticeArticle = (data: unknown): ApiNoticeArticle['get']['resp }; }; +type Article = Omit; +type ArticleKeys = keyof Article; + +const arrayOfAllArticleKeys = arrayOfAll(); + +const checkArticle = (data: unknown): Article => { + if (!isObject(data)) throw new AxiosError(`NoticeArticles-Article does not have correct type: object`); + + const keys = arrayOfAllArticleKeys(['id', 'author', 'title', 'createdDate', 'lastModifiedDate']); + if (!hasOwnProperties(data, keys)) throw new AxiosError('NoticeArticles-Article does not have some properties'); + + return { + id: checkType(data.id, isNumber), + author: checkMember(data.author), + title: checkType(data.title, isString), + createdDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), + }; +}; + type NoticeArticlesKeys = keyof ApiNoticeArticles['get']['responseData']; const arrayOfAllNoticeArticlesKeys = arrayOfAll(); @@ -36,7 +58,7 @@ export const checkNoticeArticles = (data: unknown): ApiNoticeArticles['get']['re if (!hasOwnProperties(data, keys)) throw new AxiosError('NoticeArticles does not have some properties'); return { - articles: checkType(data.articles, isArray).map(article => checkNoticeArticle(article)), + articles: checkType(data.articles, isArray).map(article => checkArticle(article)), currentPage: checkType(data.currentPage, isNumber) + 1, lastPage: checkType(data.lastPage, isNumber), totalCount: checkType(data.totalCount, isNumber), From 51752f2766ef58b0ebaf492f9d2d977bbb3f61c1 Mon Sep 17 00:00:00 2001 From: Donggyu Date: Wed, 12 Oct 2022 15:14:00 +0900 Subject: [PATCH 07/13] =?UTF-8?q?hotfix:=20=ED=83=9C=EA=B7=B8=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80=20(#412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/service/request/StudyRequest.java | 1 + .../study/CreatingStudyAcceptanceTest.java | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java index dac0b2930..e95f5f1cd 100644 --- a/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java +++ b/backend/src/main/java/com/woowacourse/moamoa/study/service/request/StudyRequest.java @@ -53,6 +53,7 @@ public class StudyRequest { @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; + @NotNull private List tagIds; public List getTagIds() { diff --git a/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java index 6bcd27700..ede4cb92c 100644 --- a/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/acceptance/test/study/CreatingStudyAcceptanceTest.java @@ -1,5 +1,6 @@ package com.woowacourse.acceptance.test.study; +import static com.woowacourse.acceptance.steps.LoginSteps.디우가; import static com.woowacourse.acceptance.steps.LoginSteps.짱구가; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @@ -140,7 +141,7 @@ void createStudy() { .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .body(Map.of("title", "제목", "excerpt", "자바를 공부하는 스터디", "thumbnail", "image", "description", "스터디 상세 설명입니다.", "startDate", LocalDate.now().plusDays(5).format( - DateTimeFormatter.ofPattern("yyyy-MM-dd")), "endDate", "")) + DateTimeFormatter.ofPattern("yyyy-MM-dd")), "endDate", "", "tagIds", List.of(1L, 3L))) .when().log().all() .post("/api/studies") .then().log().all() @@ -149,4 +150,21 @@ void createStudy() { assertThat(location).matches(Pattern.compile("/api/studies/\\d+")); } + + @DisplayName("태그 없이 스터디를 생성하는 경우 예외가 발생한다.") + @Test + void validateTagNull() { + final String jwtToken = 디우가().로그인한다(); + + RestAssured.given(spec).log().all() + .header(AUTHORIZATION, jwtToken) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .body(Map.of("title", "제목", "excerpt", "자바를 공부하는 스터디", "thumbnail", "image", + "description", "스터디 상세 설명입니다.", "startDate", LocalDate.now().plusDays(5).format( + DateTimeFormatter.ofPattern("yyyy-MM-dd")), "endDate", "")) + .when().log().all() + .post("/api/studies") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } } From 0e980b93c01143e9a1c659318244c815e0e95c2b Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Wed, 12 Oct 2022 15:33:17 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20api=20=EC=9E=98=EB=AA=BB=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=98=EB=90=9C=20=EC=86=8D=EC=84=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/link/typeChecker.ts | 2 +- frontend/src/api/studies/typeChecker.ts | 4 ++-- frontend/src/api/study/typeChecker.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/link/typeChecker.ts b/frontend/src/api/link/typeChecker.ts index f48541599..9c874f541 100644 --- a/frontend/src/api/link/typeChecker.ts +++ b/frontend/src/api/link/typeChecker.ts @@ -22,6 +22,6 @@ export const checkLink = (data: unknown): Link => { linkUrl: checkType(data.linkUrl, isString), description: checkType(data.description, isString), createdDate: checkType(data.createdDate, isDateYMD), - lastModifiedDate: checkType(data.createdDate, isDateYMD), + lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD), }; }; diff --git a/frontend/src/api/studies/typeChecker.ts b/frontend/src/api/studies/typeChecker.ts index e4fde0726..395be45a8 100644 --- a/frontend/src/api/studies/typeChecker.ts +++ b/frontend/src/api/studies/typeChecker.ts @@ -46,8 +46,8 @@ const checkStudy = (data: unknown): Study => { return { id: checkType(data.id, isNumber), title: checkType(data.title, isString), - excerpt: checkType(data.title, isString), - thumbnail: checkType(data.title, isString), + excerpt: checkType(data.excerpt, isString), + thumbnail: checkType(data.thumbnail, isString), tags: checkType(data.tags, isArray).map(tag => checkMainStudyTag(tag)), recruitmentStatus: checkType(data.recruitmentStatus, isRecruitmentStatus), }; diff --git a/frontend/src/api/study/typeChecker.ts b/frontend/src/api/study/typeChecker.ts index 983fb7e53..9034609c3 100644 --- a/frontend/src/api/study/typeChecker.ts +++ b/frontend/src/api/study/typeChecker.ts @@ -78,7 +78,7 @@ export const checkStudy = (data: unknown): ApiStudy['get']['responseData'] => { thumbnail: checkType(data.thumbnail, isString), recruitmentStatus: checkType(data.recruitmentStatus, isRecruitmentStatus), description: checkType(data.description, isString), - currentMemberCount: checkType(data.id, isNumber), + currentMemberCount: checkType(data.currentMemberCount, isNumber), maxMemberCount: checkType(data.maxMemberCount, isNumber, true), createdDate: checkType(data.createdDate, isDateYMD), enrollmentEndDate: checkType(data.enrollmentEndDate, isDateYMD, true), From b041135b07dedd61d4ce36d7f6614aff9ce40e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=98=81?= Date: Fri, 14 Oct 2022 22:06:20 +0900 Subject: [PATCH 09/13] Update deploy-backend-dev.yml --- .github/workflows/deploy-backend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-backend-dev.yml b/.github/workflows/deploy-backend-dev.yml index f56560b4c..b5f324fdf 100644 --- a/.github/workflows/deploy-backend-dev.yml +++ b/.github/workflows/deploy-backend-dev.yml @@ -54,4 +54,4 @@ jobs: - name : Deploy run: | - curl ${{ secrets.DEPLOY_BACKEND_REQUEST_URL }} + curl ${{ secrets.DEPLOY_DEV_BACKEND_REQUEST_URL }} From f27443c66cfb94af626a25dfdb2f9a087c3ea090 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Sun, 16 Oct 2022 02:11:46 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[FE]=20issue419:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20=EB=B2=84=EA=B7=B8=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=88=98=EC=A0=95=20(#423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 스터디 상세 페이지 소개글 overflow시 가로 스크롤되도록 수정 * fix: 글쓰기/미리보기 컴포넌트 overflow시 스크롤 되도록 수정 * fix: subject tag validation 조건 추가 - 한 개 이상을 선택해야 생성할 수 있도록 변경 * feat: 누구나 스터디방 페이지에 접근할 수 있도록 수정 - 상세 페이지 가입하지 않았거나 모집완료일 때 이동하기 버튼 보여주기 - 스터디 방 페이지 접속시 Read만 가능하도록 수정 * fix: maxMemberCount가 string 타입으로 전송되는 오류 수정 * feat: 로그인 성공시 이전 페이지로 이동하도록 수정 * fix: 스터디 종료 날짜보다 스터디 모집 마감 날짜가 이후인 오류 수정 * feat: 스터디 수정/생성 submit 시 오류가 있으면 alert하도록 수정 * test: history pop시 cypress CORS 에러 수정 - 테스트 오류 원인: 로그인 페이지 접속 후 로그인에 성공하면 뒤 페이지로 이동하는데, cypress가 이 때문에 CORS에러를 발생시킴 - 테스트 오류 해결: 로그인 페이지 접속하지 않고 액세스 토큰을 직접 설정 * test: cypress 오류 수정 --- .../e2e/CreateStudyFormValidation.cy.tsx | 9 ++- frontend/src/App.tsx | 32 ++++----- .../markdown-render/MarkdownRender.tsx | 2 +- .../multi-tag-select/MultiTagSelect.style.tsx | 68 +++++++++---------- .../multi-tag-select/MultiTagSelect.tsx | 13 ++-- frontend/src/constants.ts | 9 +++ .../src/context/search/SearchProvider.tsx | 4 +- .../src/context/userInfo/UserInfoProvider.tsx | 4 +- .../src/context/userRole/UserRoleProvider.tsx | 26 +++++++ frontend/src/hooks/useAuth.ts | 1 + frontend/src/hooks/useForm.tsx | 22 ++++-- frontend/src/hooks/useUserInfo.ts | 2 +- frontend/src/hooks/useUserRole.ts | 26 +++++++ frontend/src/index.tsx | 11 +-- .../description-tab/DescriptionTab.tsx | 2 +- .../enrollment-end-date/EnrollmentEndDate.tsx | 32 +++++++-- .../components/period/Period.tsx | 2 +- .../components/subject/Subject.tsx | 26 ++++++- .../components/title/Title.tsx | 6 +- .../hooks/useCreateStudyPage.ts | 10 ++- frontend/src/pages/detail-page/DetailPage.tsx | 11 +-- .../study-float-box/StudyFloatBox.tsx | 16 ++--- .../StudyWideFloatBox.tsx | 16 ++--- .../pages/detail-page/hooks/useDetailPage.ts | 19 +++--- .../edit-study-page/hooks/useEditStudyPage.ts | 10 ++- .../hooks/useLoginRedirectPage.ts | 4 +- .../pages/study-room-page/StudyRoomPage.tsx | 36 +++++----- .../components/side-menu/SideMenu.style.tsx | 3 +- .../hooks/useStudyRoomPage.tsx | 5 -- .../community-tab-panel/CommunityTabPanel.tsx | 43 ++++++------ .../components/edit-content/EditContent.tsx | 2 +- .../publish-content/PublishContent.tsx | 2 +- .../link-room-tab-panel/LinkRoomTabPanel.tsx | 13 ++-- .../hooks/useLinkRoomTabPanel.ts | 12 +++- .../tabs/notice-tab-panel/NoticeTabPanel.tsx | 38 +++++------ .../components/edit-content/EditContent.tsx | 2 +- .../notice-tab-panel/components/edit/Edit.tsx | 12 ---- .../publish-content/PublishContent.tsx | 2 +- .../components/publish/Publish.tsx | 19 ------ .../tabs/review-tab-panel/ReviewTabPanel.tsx | 14 +++- 40 files changed, 359 insertions(+), 227 deletions(-) create mode 100644 frontend/src/context/userRole/UserRoleProvider.tsx create mode 100644 frontend/src/hooks/useUserRole.ts diff --git a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx index 1e57a7434..5a3d11f71 100644 --- a/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx +++ b/frontend/cypress/e2e/CreateStudyFormValidation.cy.tsx @@ -1,5 +1,7 @@ import { DESCRIPTION_LENGTH, EXCERPT_LENGTH, PATH, TITLE_LENGTH } from '@constants'; +import AccessTokenController from '@auth/accessTokenController'; + const studyTitle = 'studyTitle'; const description = 'description'; const excerpt = 'excerpt'; @@ -8,12 +10,17 @@ const startDate = 'startDate'; describe('스터디 개설 페이지 폼 유효성 테스트', () => { before(() => { - cy.visit(`${PATH.LOGIN}?code=hihihih`).then(() => { + AccessTokenController.save('asdfasdf', 30 * 1000); + cy.visit(PATH.MAIN).then(() => { cy.wait(1000); cy.visit(PATH.CREATE_STUDY); }); }); + after(() => { + AccessTokenController.removeAccessToken(); + }); + beforeEach(() => { cy.findByPlaceholderText('*스터디 이름').as(studyTitle); cy.findByPlaceholderText('*스터디 소개글(20000자 제한)').as(description); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 54d9a4bbc..1027ad4ea 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -50,23 +50,23 @@ const App = () => { } /> } /> } /> - }> - {/* TODO: 인덱스 페이지를 따로 두면 좋을 것 같다. */} - } /> - }> - {[PATH.NOTICE_PUBLISH, PATH.NOTICE_ARTICLE(), PATH.NOTICE_EDIT()].map((path, index) => ( - } /> - ))} - - }> - {[PATH.COMMUNITY_PUBLISH, PATH.COMMUNITY_ARTICLE(), PATH.COMMUNITY_EDIT()].map((path, index) => ( - } /> - ))} - - } /> - } /> - } /> + + }> + {/* TODO: 인덱스 페이지(HOME)를 따로 두면 좋을 것 같다. */} + } /> + }> + {[PATH.NOTICE_PUBLISH, PATH.NOTICE_ARTICLE(), PATH.NOTICE_EDIT()].map((path, index) => ( + } /> + ))} + + }> + {[PATH.COMMUNITY_PUBLISH, PATH.COMMUNITY_ARTICLE(), PATH.COMMUNITY_EDIT()].map((path, index) => ( + } /> + ))} + } /> + } /> + } /> } /> diff --git a/frontend/src/components/markdown-render/MarkdownRender.tsx b/frontend/src/components/markdown-render/MarkdownRender.tsx index 869523d71..94fe668ca 100644 --- a/frontend/src/components/markdown-render/MarkdownRender.tsx +++ b/frontend/src/components/markdown-render/MarkdownRender.tsx @@ -22,7 +22,7 @@ const MarkdownRender = ({ markdownContent }: MarkdownRenderProps) => { }, [contentRef, markdownContent]); return ( -
+
` + ${({ theme, invalid }) => css` + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + + position: relative; + padding-right: 8px; + min-height: 38px; + + border: 1px solid ${theme.colors.secondary.base}; + background-color: ${theme.colors.secondary.light}; + border-radius: ${theme.radius.sm}; + + ${invalid && + css` + border: 1px solid ${theme.colors.red}; + + &:focus { + border: 1px solid ${theme.colors.red}; + } + `} + `} `; export const SelectedOptionList = styled.ul` - position: relative; - display: flex; - flex: 1 1 0%; + flex: 1 1 0; flex-wrap: wrap; align-items: center; - -webkit-box-align: center; + position: relative; + padding: 2px 8px; overflow: hidden; `; @@ -46,22 +51,22 @@ export const SelectedOption = styled.li` ${({ theme }) => css` display: flex; + margin: 2px; + background-color: ${theme.colors.secondary.base}; border-radius: 2px; - margin: 2px; font-size: 14px; `} `; export const SelectedOptionValue = styled.div` padding: 3px 3px 3px 6px; - font-size: 12px; + font-size: 12px; border-radius: 2px; - color: rgb(51, 51, 51); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; `; export const UnselectButton = styled(UnstyledButton)` @@ -70,7 +75,6 @@ export const UnselectButton = styled(UnstyledButton)` font-size: 14px; - -webkit-box-align: center; border-radius: 2px; padding-left: 4px; padding-right: 4px; @@ -80,7 +84,6 @@ export const Indicators = styled.div` display: flex; align-items: center; align-self: stretch; - -webkit-box-align: center; flex-shrink: 0; row-gap: 10px; @@ -88,18 +91,14 @@ export const Indicators = styled.div` export const DropDown = styled(ImportedDropDownBox)` max-height: 180px; - background-color: white; - box-shadow: 0px 0px 4px 0px; - - border-radius: 4px; `; export const UnselectedOption = styled.li` ${({ theme }) => css` font-size: 20px; + &:hover { - background-color: ${theme.colors.secondary.light}; // TODO: 색 세분화 필요 - color: white; + background-color: ${theme.colors.white}; // TODO: 색 세분화 필요 } `} `; @@ -108,6 +107,7 @@ export const SelectButton = styled(UnstyledButton)` width: 100%; height: 100%; padding: 10px; + text-align: left; `; diff --git a/frontend/src/components/multi-tag-select/MultiTagSelect.tsx b/frontend/src/components/multi-tag-select/MultiTagSelect.tsx index db9e61593..084555377 100644 --- a/frontend/src/components/multi-tag-select/MultiTagSelect.tsx +++ b/frontend/src/components/multi-tag-select/MultiTagSelect.tsx @@ -2,6 +2,8 @@ import { forwardRef, useEffect, useRef, useState } from 'react'; import { isFunction, isObject } from '@utils'; +import { type UseFormRegisterReturn } from '@hooks/useForm'; + import Center from '@components/center/Center'; import DownArrowIcon from '@components/icons/down-arrow-icon/DownArrowIcon'; import XMarkIcon from '@components/icons/x-mark-icon/XMarkIcon'; @@ -13,13 +15,13 @@ type Option = { }; export type MultiTagSelectProps = { - name: string; options: Array
-
+
diff --git a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx index 81717d2cf..eb85d3f29 100644 --- a/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx +++ b/frontend/src/pages/create-study-page/components/enrollment-end-date/EnrollmentEndDate.tsx @@ -1,17 +1,19 @@ import { useMemo } from 'react'; -import { getNextYear, getToday } from '@utils'; +import { getNextYear, getToday, isDateYMD } from '@utils'; import { compareDateTime } from '@utils/dates'; -import type { StudyDetail } from '@custom-types'; +import type { DateYMD, StudyDetail } from '@custom-types'; -import { useFormContext } from '@hooks/useForm'; +import { makeValidationResult, useFormContext } from '@hooks/useForm'; import Flex from '@components/flex/Flex'; import Input from '@components/input/Input'; import Label from '@components/label/Label'; import MetaBox from '@components/meta-box/MetaBox'; +import { END_DATE } from '@create-study-page/components/period/Period'; + export type PeriodProps = { originalEnrollmentEndDate?: StudyDetail['enrollmentEndDate']; }; @@ -19,13 +21,20 @@ export type PeriodProps = { const ENROLLMENT_END_DATE = 'enrollment-end-date'; const EnrollmentEndDate: React.FC = ({ originalEnrollmentEndDate }) => { - const { register } = useFormContext(); + const { + register, + getField, + formState: { errors }, + } = useFormContext(); + const today = useMemo(() => getToday(), []); const minEndDate = originalEnrollmentEndDate ? compareDateTime(originalEnrollmentEndDate, today) : today; const maxEndDate = getNextYear( originalEnrollmentEndDate ? compareDateTime(originalEnrollmentEndDate, today, 'max') : today, ); + const isValid = !errors[ENROLLMENT_END_DATE]?.hasError; + return ( 스터디 신청 마감일 @@ -35,8 +44,23 @@ const EnrollmentEndDate: React.FC = ({ originalEnrollmentEndDate }) { + const studyEndDateElement = getField(END_DATE); + if (!studyEndDateElement) + return makeValidationResult(true, ' %ERROR% 스터디 종료일 입력 요소가 존재하지 않습니다.'); + + const studyEndDate = studyEndDateElement.fieldElement.value; + if (!studyEndDate) return makeValidationResult(false); + if (!isDateYMD(studyEndDate)) return makeValidationResult(true, '잘못된 형식입니다.'); + + if (compareDateTime(studyEndDate, value) === studyEndDate) + return makeValidationResult(true, '스터디 신청 마감일은 스터디 종료일 이전이어야 합니다.'); + + return makeValidationResult(false); + }, min: minEndDate, max: maxEndDate, })} diff --git a/frontend/src/pages/create-study-page/components/period/Period.tsx b/frontend/src/pages/create-study-page/components/period/Period.tsx index 7ac088b04..3fc12d18b 100644 --- a/frontend/src/pages/create-study-page/components/period/Period.tsx +++ b/frontend/src/pages/create-study-page/components/period/Period.tsx @@ -18,7 +18,7 @@ type PeriodProps = { }; const START_DATE = 'start-date'; -const END_DATE = 'end-date'; +export const END_DATE = 'end-date'; const Period: React.FC = ({ originalStartDate, originalEndDate }) => { const { register } = useFormContext(); diff --git a/frontend/src/pages/create-study-page/components/subject/Subject.tsx b/frontend/src/pages/create-study-page/components/subject/Subject.tsx index 692db62f6..a3a55885c 100644 --- a/frontend/src/pages/create-study-page/components/subject/Subject.tsx +++ b/frontend/src/pages/create-study-page/components/subject/Subject.tsx @@ -1,8 +1,10 @@ +import { SUBJECT_TAG_COUNT } from '@constants'; + import type { StudyDetail } from '@custom-types'; import { useGetTags } from '@api/tags'; -import { useFormContext } from '@hooks/useForm'; +import { makeValidationResult, useFormContext } from '@hooks/useForm'; import Label from '@components/label/Label'; import MetaBox from '@components/meta-box/MetaBox'; @@ -22,11 +24,16 @@ const subjectsToOptions = (subjects: StudyDetail['tags']): MultiTagSelectProps[' }; const Subject: React.FC = ({ originalSubjects }) => { - const { register } = useFormContext(); + const { + register, + formState: { errors }, + } = useFormContext(); const { data, isLoading, isError, isSuccess } = useGetTags(); const originalOptions = originalSubjects ? subjectsToOptions(originalSubjects) : null; // null로 해야 아래쪽 삼항 연산자가 작동합니다 + const isValid = !errors[SUBJECT]?.hasError; + const render = () => { if (isLoading) return
loading...
; @@ -46,7 +53,20 @@ const Subject: React.FC = ({ originalSubjects }) => { const options = subjectsToOptions(subjects); - return ; + return ( + { + if (!val || val.length === 0) return makeValidationResult(true, SUBJECT_TAG_COUNT.MIN.MESSAGE); + return makeValidationResult(false); + }, + minLength: SUBJECT_TAG_COUNT.MIN.VALUE, + })} + /> + ); }; return ( diff --git a/frontend/src/pages/create-study-page/components/title/Title.tsx b/frontend/src/pages/create-study-page/components/title/Title.tsx index c8cafd557..132fc814d 100644 --- a/frontend/src/pages/create-study-page/components/title/Title.tsx +++ b/frontend/src/pages/create-study-page/components/title/Title.tsx @@ -18,10 +18,12 @@ export type TitleProps = { const TITLE = 'title'; const Title: React.FC = ({ originalTitle }) => { - const { register, formState } = useFormContext(); + const { + register, + formState: { errors }, + } = useFormContext(); const { count, setCount, maxCount } = useLetterCount(TITLE_LENGTH.MAX.VALUE, originalTitle?.length ?? 0); - const { errors } = formState; const isValid = !errors[TITLE]?.hasError; const handleTitleChange = ({ target: { value } }: React.ChangeEvent) => setCount(value.length); diff --git a/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts b/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts index d25dfa297..0dde681ba 100644 --- a/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts +++ b/frontend/src/pages/create-study-page/hooks/useCreateStudyPage.ts @@ -29,9 +29,15 @@ const useCreateStudyPage = () => { const { mutateAsync } = usePostStudy(); const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { - if (!submitResult.values) return; + const { values, errors } = submitResult; + if (!values || !errors) return; + + if (Object.values(errors).some(error => error.hasError)) { + const error = Object.values(errors).find(error => error.hasError); + error && alert(error.errorMessage); + return; + } - const { values } = submitResult; const { feTagId, beTagId } = getAreaTagId(); const subject = values['subject'].split(COMMA); const tagIds = [ diff --git a/frontend/src/pages/detail-page/DetailPage.tsx b/frontend/src/pages/detail-page/DetailPage.tsx index ca6989a59..650d8fe45 100644 --- a/frontend/src/pages/detail-page/DetailPage.tsx +++ b/frontend/src/pages/detail-page/DetailPage.tsx @@ -20,7 +20,7 @@ import StudyWideFloatBox from '@detail-page/components/study-wide-float-box/Stud import useDetailPage from '@detail-page/hooks/useDetailPage'; const DetailPage: React.FC = () => { - const { studyId, detailQueryResult, userRoleQueryResult, handleRegisterButtonClick } = useDetailPage(); + const { studyId, detailQueryResult, isOwner, isOwnerOrMember, handleRegisterButtonClick } = useDetailPage(); const { isFetching, isSuccess, isError, data } = detailQueryResult; if (!studyId) { @@ -60,11 +60,12 @@ const DetailPage: React.FC = () => { startDate={startDate} endDate={endDate} tags={tags} - isOwner={userRoleQueryResult.data?.role === 'OWNER'} + isOwner={isOwner} /> -
+ {/* TODO: UI 버그 수정 -> overflow-auto 적용! 수정시 이 주석은 지워주세요. */} +
@@ -75,7 +76,7 @@ const DetailPage: React.FC = () => {
{ & { studyId: number; - userRole?: UserRole; + isOwnerOrMember: boolean; ownerName: string; onRegisterButtonClick: React.MouseEventHandler; }; const StudyFloatBox: React.FC = ({ studyId, - userRole, + isOwnerOrMember, enrollmentEndDate, currentMemberCount, maxMemberCount, @@ -36,7 +36,7 @@ const StudyFloatBox: React.FC = ({ const isOpen = recruitmentStatus === RECRUITMENT_STATUS.START; const renderEnrollmentEndDateContent = () => { - if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { + if (isOwnerOrMember) { return 이미 가입한 스터디입니다; } @@ -57,7 +57,7 @@ const StudyFloatBox: React.FC = ({ }; const renderButton = () => { - if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { + if (isOwnerOrMember || !isOpen) { return ( @@ -68,8 +68,8 @@ const StudyFloatBox: React.FC = ({ } return ( - - {isOpen ? '스터디 가입하기' : '모집이 마감되었습니다'} + + 스터디 가입하기 ); }; diff --git a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx index b122733af..cc34f0eb5 100644 --- a/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx +++ b/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.tsx @@ -1,11 +1,11 @@ import { Link } from 'react-router-dom'; -import { PATH, RECRUITMENT_STATUS, USER_ROLE } from '@constants'; +import { PATH, RECRUITMENT_STATUS } from '@constants'; import { yyyymmddTommdd } from '@utils'; import tw from '@utils/tw'; -import type { StudyDetail, UserRole } from '@custom-types'; +import type { StudyDetail } from '@custom-types'; import { theme } from '@styles/theme'; @@ -18,13 +18,13 @@ export type StudyWideFloatBoxProps = Pick< 'enrollmentEndDate' | 'currentMemberCount' | 'maxMemberCount' | 'recruitmentStatus' > & { studyId: number; - userRole?: UserRole; + isOwnerOrMember: boolean; onRegisterButtonClick: React.MouseEventHandler; }; const StudyWideFloatBox: React.FC = ({ studyId, - userRole, + isOwnerOrMember, enrollmentEndDate, currentMemberCount, maxMemberCount, @@ -34,7 +34,7 @@ const StudyWideFloatBox: React.FC = ({ const isOpen = recruitmentStatus === RECRUITMENT_STATUS.START; const renderEnrollmentEndDateContent = () => { - if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { + if (isOwnerOrMember) { return 이미 가입한 스터디입니다; } @@ -54,7 +54,7 @@ const StudyWideFloatBox: React.FC = ({ }; const renderButton = () => { - if (userRole === USER_ROLE.MEMBER || userRole === USER_ROLE.OWNER) { + if (isOwnerOrMember || !isOpen) { return ( @@ -65,8 +65,8 @@ const StudyWideFloatBox: React.FC = ({ } return ( - - {isOpen ? '가입하기' : '모집 마감'} + + 가입하기 ); }; diff --git a/frontend/src/pages/detail-page/hooks/useDetailPage.ts b/frontend/src/pages/detail-page/hooks/useDetailPage.ts index c34c92002..7429a46cd 100644 --- a/frontend/src/pages/detail-page/hooks/useDetailPage.ts +++ b/frontend/src/pages/detail-page/hooks/useDetailPage.ts @@ -1,20 +1,22 @@ import { useParams } from 'react-router-dom'; -import { useGetUserRole } from '@api/member'; import { usePostMyStudy } from '@api/my-study'; import { useGetStudy } from '@api/study'; import { useAuth } from '@hooks/useAuth'; +import { useUserRole } from '@hooks/useUserRole'; const useDetailPage = () => { - const { studyId } = useParams() as { studyId: string }; + const { studyId: _studyId } = useParams<{ studyId: string }>(); + const studyId = Number(_studyId); + const { isLoggedIn } = useAuth(); - const detailQueryResult = useGetStudy({ studyId: Number(studyId) }); + const detailQueryResult = useGetStudy({ studyId }); const { mutate } = usePostMyStudy(); - const userRoleQueryResult = useGetUserRole({ - studyId: Number(studyId), + const { isOwner, isOwnerOrMember, fetchUserRole } = useUserRole({ + studyId, options: { enabled: isLoggedIn, }, @@ -27,7 +29,7 @@ const useDetailPage = () => { } mutate( - { studyId: Number(studyId) }, + { studyId }, { onError: () => { alert('가입에 실패했습니다.'); @@ -35,7 +37,7 @@ const useDetailPage = () => { onSuccess: () => { alert('가입했습니다 :D'); detailQueryResult.refetch(); - userRoleQueryResult.refetch(); + fetchUserRole(); }, }, ); @@ -43,7 +45,8 @@ const useDetailPage = () => { return { detailQueryResult, - userRoleQueryResult, + isOwner, + isOwnerOrMember, studyId, handleRegisterButtonClick, }; diff --git a/frontend/src/pages/edit-study-page/hooks/useEditStudyPage.ts b/frontend/src/pages/edit-study-page/hooks/useEditStudyPage.ts index b79fe0326..e18849341 100644 --- a/frontend/src/pages/edit-study-page/hooks/useEditStudyPage.ts +++ b/frontend/src/pages/edit-study-page/hooks/useEditStudyPage.ts @@ -32,9 +32,15 @@ const useEditStudyPage = () => { }; const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { - if (!submitResult.values) return; + const { values, errors } = submitResult; + if (!values || !errors) return; + + if (Object.values(errors).some(error => error.hasError)) { + const error = Object.values(errors).find(error => error.hasError); + error && alert(error.errorMessage); + return; + } - const { values } = submitResult; const { feTagId, beTagId } = getAreaTagId(); const subject = values['subject'].split(COMMA); const tagIds = [ diff --git a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts index 40a99375d..49dd78526 100644 --- a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts +++ b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts @@ -9,7 +9,7 @@ import { useAuth } from '@hooks/useAuth'; const useLoginRedirectPage = () => { const [searchParams] = useSearchParams(); - const codeParam = searchParams.get('code') as string; + const codeParam = searchParams.get('code'); const navigate = useNavigate(); const { login } = useAuth(); @@ -32,7 +32,7 @@ const useLoginRedirectPage = () => { }, onSuccess: ({ accessToken, expiredTime }) => { login(accessToken, expiredTime); - navigate(PATH.MAIN, { replace: true }); + navigate(-1); }, }, ); diff --git a/frontend/src/pages/study-room-page/StudyRoomPage.tsx b/frontend/src/pages/study-room-page/StudyRoomPage.tsx index 3eefdb37a..c20fe6f25 100644 --- a/frontend/src/pages/study-room-page/StudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/StudyRoomPage.tsx @@ -1,8 +1,8 @@ -import { Navigate, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; -import { PATH, USER_ROLE } from '@constants'; +import styled from '@emotion/styled'; -import tw from '@utils/tw'; +import { mqDown } from '@utils'; import Flex from '@components/flex/Flex'; import Wrapper from '@components/wrapper/Wrapper'; @@ -11,29 +11,29 @@ import SideMenu from '@study-room-page/components/side-menu/SideMenu'; import useStudyRoomPage from '@study-room-page/hooks/useStudyRoomPage'; const StudyRoomPage: React.FC = () => { - const { tabs, activeTabId, userRoleQueryResult, handleTabButtonClick } = useStudyRoomPage(); - const { data, isError, isSuccess } = userRoleQueryResult; - - if (isSuccess && data.role === USER_ROLE.NON_MEMBER) { - alert('잘못된 접근입니다.'); - return ; - } - - if (isError) { - alert('오류가 발생했습니다.'); - return ; - } + const { tabs, activeTabId, handleTabButtonClick } = useStudyRoomPage(); return ( - + -
+ -
+
); }; export default StudyRoomPage; + +const sidebarWidth = 180; +const MainSection = styled.section` + flex-grow: 1; + + max-width: calc(100% - ${sidebarWidth}px); + + ${mqDown('lg')} { + max-width: 100%; + } +`; diff --git a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx index 320863a10..d134f9b9f 100644 --- a/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx +++ b/frontend/src/pages/study-room-page/components/side-menu/SideMenu.style.tsx @@ -10,8 +10,7 @@ export const Sidebar = styled.nav` left: 0; z-index: 1; - width: 100%; - max-width: 180px; + width: 180px; padding: 16px; background-color: ${theme.colors.secondary.light}; diff --git a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx index e4190c320..11efa4722 100644 --- a/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx +++ b/frontend/src/pages/study-room-page/hooks/useStudyRoomPage.tsx @@ -3,8 +3,6 @@ import { useLocation, useParams } from 'react-router-dom'; import { PATH } from '@constants'; -import { useGetUserRole } from '@api/member'; - export type TabId = typeof PATH[keyof Pick]; export type Tab = { id: TabId; name: string }; export type Tabs = Array; @@ -15,8 +13,6 @@ const useStudyRoomPage = () => { const { studyId: _studyId } = useParams<{ studyId: string }>(); const studyId = Number(_studyId); - const userRoleQueryResult = useGetUserRole({ studyId }); - const tabs: Tabs = [ { id: PATH.NOTICE, name: '공지사항' }, { id: PATH.COMMUNITY, name: '게시판' }, @@ -37,7 +33,6 @@ const useStudyRoomPage = () => { studyId, tabs, activeTabId, - userRoleQueryResult, handleTabButtonClick, }; }; diff --git a/frontend/src/pages/study-room-page/tabs/community-tab-panel/CommunityTabPanel.tsx b/frontend/src/pages/study-room-page/tabs/community-tab-panel/CommunityTabPanel.tsx index c95ea89b7..fd7f620b5 100644 --- a/frontend/src/pages/study-room-page/tabs/community-tab-panel/CommunityTabPanel.tsx +++ b/frontend/src/pages/study-room-page/tabs/community-tab-panel/CommunityTabPanel.tsx @@ -1,9 +1,11 @@ -import { Link, useLocation, useParams } from 'react-router-dom'; +import { Link, Navigate, useLocation, useParams } from 'react-router-dom'; import { PATH } from '@constants'; import { theme } from '@styles/theme'; +import { useUserRole } from '@hooks/useUserRole'; + import { TextButton } from '@components/button'; import Divider from '@components/divider/Divider'; import Flex from '@components/flex/Flex'; @@ -19,6 +21,8 @@ const CommunityTabPanel: React.FC = () => { const { studyId: _studyId, articleId: _articleId } = useParams<{ studyId: string; articleId: string }>(); const [studyId, articleId] = [Number(_studyId), Number(_articleId)]; + const { isNonMember, isOwnerOrMember } = useUserRole({ studyId }); + const lastPath = location.pathname.split('/').at(-1); const isPublishPage = lastPath === 'publish'; const isEditPage = lastPath === 'edit'; @@ -28,13 +32,15 @@ const CommunityTabPanel: React.FC = () => { const renderArticleListPage = () => { return ( <> - - - - 글쓰기 - - - + {isOwnerOrMember && ( + + + + 글쓰기 + + + + )} @@ -42,18 +48,15 @@ const CommunityTabPanel: React.FC = () => { }; const render = () => { - if (isListPage) { - return renderArticleListPage(); - } - if (isArticleDetailPage) { - return
; - } - if (isPublishPage) { - return ; - } - if (isEditPage) { - return ; - } + if (isListPage) return renderArticleListPage(); + + if (isArticleDetailPage) return
; + + if (isNonMember) return ; + + if (isPublishPage) return ; + + if (isEditPage) return ; }; return ( diff --git a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/edit-content/EditContent.tsx b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/edit-content/EditContent.tsx index 84eac7dcc..8065a72ca 100644 --- a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/edit-content/EditContent.tsx +++ b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/edit-content/EditContent.tsx @@ -80,7 +80,7 @@ const EditContent: React.FC = ({ content }) => { })} >
-
+
diff --git a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/publish-content/PublishContent.tsx b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/publish-content/PublishContent.tsx index e1ab26ff9..1c15acf90 100644 --- a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/publish-content/PublishContent.tsx +++ b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/publish-content/PublishContent.tsx @@ -75,7 +75,7 @@ const PublishContent = () => { })} >
-
+
diff --git a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/LinkRoomTabPanel.tsx b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/LinkRoomTabPanel.tsx index 4112177ca..9a59d64f9 100644 --- a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/LinkRoomTabPanel.tsx +++ b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/LinkRoomTabPanel.tsx @@ -3,6 +3,7 @@ import tw from '@utils/tw'; import type { Link } from '@custom-types'; import { TextButton } from '@components/button'; +import Divider from '@components/divider/Divider'; import InfiniteScroll from '@components/infinite-scroll/InfiniteScroll'; import ModalPortal from '@components/modal/Modal'; import Wrapper from '@components/wrapper/Wrapper'; @@ -18,6 +19,7 @@ const LinkRoomTabPanel: React.FC = () => { userInfo, infiniteLinksQueryResult, isModalOpen, + isOwnerOrMember, handleLinkAddButtonClick, handleModalClose, handlePostLinkError, @@ -57,11 +59,14 @@ const LinkRoomTabPanel: React.FC = () => { return ( -
- - 링크 추가하기 - +
+ {isOwnerOrMember && ( + + 링크 추가하기 + + )}
+ {renderLinkList()} {isModalOpen && ( diff --git a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts index 68ce51954..1c518c5e8 100644 --- a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts +++ b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts @@ -4,11 +4,16 @@ import { useParams } from 'react-router-dom'; import { useGetInfiniteLinks } from '@api/links'; import { useUserInfo } from '@hooks/useUserInfo'; +import { useUserRole } from '@hooks/useUserRole'; export const useLinkRoomTabPanel = () => { - const { studyId } = useParams<{ studyId: string }>(); + const { studyId: _studyId } = useParams<{ studyId: string }>(); + const studyId = Number(_studyId); + const { fetchUserInfo, userInfo } = useUserInfo(); - const infiniteLinksQueryResult = useGetInfiniteLinks({ studyId: Number(studyId) }); + const { isOwnerOrMember } = useUserRole({ studyId }); + + const infiniteLinksQueryResult = useGetInfiniteLinks({ studyId }); const [isModalOpen, setIsModalOpen] = useState(false); @@ -31,10 +36,11 @@ export const useLinkRoomTabPanel = () => { }; return { - studyId: Number(studyId), + studyId, userInfo, infiniteLinksQueryResult, isModalOpen, + isOwnerOrMember, handleLinkAddButtonClick, handleModalClose, handlePostLinkSuccess, diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/NoticeTabPanel.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/NoticeTabPanel.tsx index 96e4f7913..dd433963e 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/NoticeTabPanel.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/NoticeTabPanel.tsx @@ -1,9 +1,11 @@ -import { Link, useLocation, useParams } from 'react-router-dom'; +import { Link, Navigate, useLocation, useParams } from 'react-router-dom'; import { PATH } from '@constants'; import { theme } from '@styles/theme'; +import { useUserRole } from '@hooks/useUserRole'; + import { TextButton } from '@components/button'; import Divider from '@components/divider/Divider'; import Flex from '@components/flex/Flex'; @@ -13,14 +15,14 @@ import ArticleList from '@notice-tab/components/article-list/ArticleList'; import Article from '@notice-tab/components/article/Article'; import Edit from '@notice-tab/components/edit/Edit'; import Publish from '@notice-tab/components/publish/Publish'; -import usePermission from '@notice-tab/hooks/usePermission'; const NoticeTabPanel: React.FC = () => { const location = useLocation(); const { studyId: _studyId, articleId: _articleId } = useParams<{ studyId: string; articleId: string }>(); const [studyId, articleId] = [Number(_studyId), Number(_articleId)]; - const { hasPermission: isOwner } = usePermission(studyId, 'OWNER'); + const { isOwner, isNonMember, isMember } = useUserRole({ studyId }); + const lastPath = location.pathname.split('/').at(-1); const isPublishPage = lastPath === 'publish'; const isEditPage = lastPath === 'edit'; @@ -30,15 +32,15 @@ const NoticeTabPanel: React.FC = () => { const renderArticleListPage = () => { return ( <> - - {isOwner && ( + {isOwner && ( + 글쓰기 - )} - + + )} @@ -46,19 +48,15 @@ const NoticeTabPanel: React.FC = () => { }; const render = () => { - if (isListPage) { - return renderArticleListPage(); - } - if (isArticleDetailPage) { - const numArticleId = Number(articleId); - return
; - } - if (isPublishPage) { - return ; - } - if (isEditPage) { - return ; - } + if (isListPage) return renderArticleListPage(); + + if (isArticleDetailPage) return
; + + if (isNonMember || isMember) return ; + + if (isPublishPage) return ; + + if (isEditPage) return ; }; return ( diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit-content/EditContent.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit-content/EditContent.tsx index 84eac7dcc..8065a72ca 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit-content/EditContent.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit-content/EditContent.tsx @@ -80,7 +80,7 @@ const EditContent: React.FC = ({ content }) => { })} >
-
+
diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit/Edit.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit/Edit.tsx index a0c737e35..c2b3460d3 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit/Edit.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/edit/Edit.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { PATH } from '@constants'; @@ -17,7 +16,6 @@ import PageTitle from '@components/page-title/PageTitle'; import EditContent from '@notice-tab/components/edit-content/EditContent'; import EditTitle from '@notice-tab/components/edit-title/EditTitle'; -import usePermission from '@notice-tab/hooks/usePermission'; export type EditProps = { studyId: StudyId; @@ -31,16 +29,6 @@ const Edit: React.FC = ({ studyId, articleId }) => { const getNoticeArticleQueryResult = useGetNoticeArticle({ studyId, articleId }); const { mutateAsync } = usePutNoticeArticle(); - const { isFetching, hasPermission } = usePermission(studyId, 'OWNER'); - - useEffect(() => { - if (isFetching) return; - if (hasPermission) return; - - alert('접근할 수 없습니다!'); - navigate(`../${PATH.NOTICE}`); - }, [studyId, navigate, isFetching, hasPermission]); - const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { const { values } = submitResult; if (!values) return; diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish-content/PublishContent.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish-content/PublishContent.tsx index e1ab26ff9..1c15acf90 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish-content/PublishContent.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish-content/PublishContent.tsx @@ -75,7 +75,7 @@ const PublishContent = () => { })} >
-
+
diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish/Publish.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish/Publish.tsx index 63decf6af..fb32b3d91 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish/Publish.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/publish/Publish.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { PATH } from '@constants'; @@ -17,7 +16,6 @@ import PageTitle from '@components/page-title/PageTitle'; import PublishContent from '@notice-tab/components/publish-content/PublishContent'; import PublishTitle from '@notice-tab/components/publish-title/PublishTitle'; -import usePermission from '@notice-tab/hooks/usePermission'; export type PublishProps = { studyId: StudyId; @@ -27,15 +25,6 @@ const Publish: React.FC = ({ studyId }) => { const formMethods = useForm(); const navigate = useNavigate(); const { mutateAsync } = usePostNoticeArticle(); - const { isFetching, isError, hasPermission } = usePermission(studyId, 'OWNER'); - - useEffect(() => { - if (isFetching) return; - if (hasPermission) return; - - alert('접근할 수 없습니다!'); - navigate(`../${PATH.NOTICE}`); - }, [studyId, navigate, isFetching, hasPermission]); const onSubmit = async (_: React.FormEvent, submitResult: UseFormSubmitResult) => { const { values } = submitResult; @@ -62,14 +51,6 @@ const Publish: React.FC = ({ studyId }) => { ); }; - if (isFetching) { - return
유저 정보 가져오는 중...
; - } - - if (isError) { - return
유저 정보를 가져오는 도중 에러가 발생했습니다.
; - } - return ( 공지사항 작성 diff --git a/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx b/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx index 3cfe38e49..570687856 100644 --- a/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx +++ b/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx @@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'; import { useGetStudyReviews } from '@api/reviews'; import { useUserInfo } from '@hooks/useUserInfo'; +import { useUserRole } from '@hooks/useUserRole'; import Divider from '@components/divider/Divider'; import Wrapper from '@components/wrapper/Wrapper'; @@ -15,8 +16,10 @@ const ReviewTabPanel: React.FC = () => { const { studyId: _studyId } = useParams<{ studyId: string }>(); const studyId = Number(_studyId); - const { data, isFetching, refetch, isError, isSuccess } = useGetStudyReviews({ studyId }); const { userInfo, fetchUserInfo } = useUserInfo(); + const { isOwnerOrMember } = useUserRole({ studyId }); + + const { data, isFetching, refetch, isError, isSuccess } = useGetStudyReviews({ studyId }); useEffect(() => { fetchUserInfo(); @@ -67,7 +70,14 @@ const ReviewTabPanel: React.FC = () => { return ( - + {isOwnerOrMember && ( + + )} {renderReviewList()} From 9ebefb6a1d02801bf89953cddf60ae8f64ed88bc Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Sun, 16 Oct 2022 03:09:34 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20401=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=9B=84=20reload=20->=20replace=20(#425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메인 페이지로 이동시킨다 --- frontend/src/api/axiosInstance.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index 8791e4dfa..def221372 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,5 +1,7 @@ import axios, { type AxiosError, type AxiosResponse } from 'axios'; +import { PATH } from '@constants'; + import { getRefreshAccessToken } from '@api/auth'; import AccessTokenController from '@auth/accessTokenController'; @@ -18,7 +20,7 @@ const handleAxiosError = (error: AxiosError<{ message: string; code?: number }>) if (error.response?.status === 401) { AccessTokenController.clear(); alert('장시간 접속하지 않아 로그아웃되었습니다.'); - window.location.reload(); + window.location.replace(PATH.MAIN); return Promise.reject(error); } From 241a9a4d52b9367f1f0c7952088714a485080bec Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Sun, 16 Oct 2022 15:53:52 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[FE]=20issue419:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EB=B0=A9=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EA=B3=B5=EA=B0=9C=20(#427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: user role, user info api 수정 * feat: 로그인 관련 훅 수정 - useAuth - useUserInfo - useUserRole enabled 옵션 변경 * feat: 로그인 관련 훅 사용하고 있는 컴포넌트 수정 * refactor: type과 상대경로 수정 --- frontend/src/api/member/index.ts | 17 +++++-- .../button-group/ButtonGroup.style.tsx | 2 +- .../button/icon-button/IconButton.style.tsx | 2 +- frontend/src/components/flex/Flex.style.tsx | 2 +- .../src/components/wrapper/Wrapper.style.tsx | 2 +- frontend/src/context/login/LoginProvider.tsx | 9 +--- frontend/src/hooks/useAuth.ts | 4 -- frontend/src/hooks/useUserInfo.ts | 16 +++++- frontend/src/hooks/useUserRole.ts | 23 +++++++-- .../pages/detail-page/hooks/useDetailPage.ts | 7 +-- .../components/article/Article.tsx | 51 +++++++++---------- .../hooks/useLinkRoomTabPanel.ts | 8 +-- .../components/article/Article.tsx | 51 +++++++++---------- .../notice-tab-panel/hooks/usePermission.ts | 16 ------ .../tabs/review-tab-panel/ReviewTabPanel.tsx | 7 +-- 15 files changed, 103 insertions(+), 114 deletions(-) delete mode 100644 frontend/src/pages/study-room-page/tabs/notice-tab-panel/hooks/usePermission.ts diff --git a/frontend/src/api/member/index.ts b/frontend/src/api/member/index.ts index 32a66d5da..a532459cc 100644 --- a/frontend/src/api/member/index.ts +++ b/frontend/src/api/member/index.ts @@ -26,6 +26,17 @@ export type ApiUserRole = { export type ApiUserInformation = { get: { responseData: Member; + variables: { + options?: Omit< + UseQueryOptions< + ApiUserInformation['get']['responseData'], + AxiosError, + ApiUserInformation['get']['responseData'], + QueryKey + >, + 'queryKey' | 'queryFn' + >; + }; }; }; @@ -48,7 +59,5 @@ export const getUserInformation = async () => { return checkUserInformation(response.data); }; -export const useGetUserInformation = () => - useQuery('user-info', getUserInformation, { - enabled: false, - }); +export const useGetUserInformation = ({ options }: ApiUserInformation['get']['variables']) => + useQuery('user-info', getUserInformation, options); diff --git a/frontend/src/components/button-group/ButtonGroup.style.tsx b/frontend/src/components/button-group/ButtonGroup.style.tsx index 8e9a42366..ca9d57fae 100644 --- a/frontend/src/components/button-group/ButtonGroup.style.tsx +++ b/frontend/src/components/button-group/ButtonGroup.style.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import type { MakeRequired } from '@custom-types'; -import { ButtonGroupProps } from './ButtonGroup'; +import { type ButtonGroupProps } from '@components/button-group/ButtonGroup'; type StyledButtonGroupProps = MakeRequired< Pick, diff --git a/frontend/src/components/button/icon-button/IconButton.style.tsx b/frontend/src/components/button/icon-button/IconButton.style.tsx index 9a08834f3..d3a323335 100644 --- a/frontend/src/components/button/icon-button/IconButton.style.tsx +++ b/frontend/src/components/button/icon-button/IconButton.style.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import type { MakeRequired } from '@custom-types'; -import { IconButtonProps } from './IconButton'; +import { type IconButtonProps } from '@components/button/icon-button/IconButton'; type StyledIconButtonProps = MakeRequired< Pick, diff --git a/frontend/src/components/flex/Flex.style.tsx b/frontend/src/components/flex/Flex.style.tsx index c362caee6..a3fd3b341 100644 --- a/frontend/src/components/flex/Flex.style.tsx +++ b/frontend/src/components/flex/Flex.style.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { FlexProps } from './Flex'; +import { type FlexProps } from '@components/flex/Flex'; type StyledFlexProps = Omit; diff --git a/frontend/src/components/wrapper/Wrapper.style.tsx b/frontend/src/components/wrapper/Wrapper.style.tsx index 916233f04..d0627de27 100644 --- a/frontend/src/components/wrapper/Wrapper.style.tsx +++ b/frontend/src/components/wrapper/Wrapper.style.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { WrapperProps } from './Wrapper'; +import { type WrapperProps } from '@components/wrapper/Wrapper'; type StyledWrapperProps = Required>; diff --git a/frontend/src/context/login/LoginProvider.tsx b/frontend/src/context/login/LoginProvider.tsx index 76e956c49..9165a98eb 100644 --- a/frontend/src/context/login/LoginProvider.tsx +++ b/frontend/src/context/login/LoginProvider.tsx @@ -1,11 +1,9 @@ -import { ReactNode, createContext, useEffect, useState } from 'react'; +import { ReactNode, createContext, useState } from 'react'; import { noop } from '@utils'; import AccessTokenController from '@auth/accessTokenController'; -import { useUserInfo } from '@hooks/useUserInfo'; - type LoginProviderProps = { children: ReactNode; }; @@ -23,11 +21,6 @@ export const LoginContext = createContext({ export const LoginProvider = ({ children }: LoginProviderProps) => { const { hasAccessToken, hasTokenDateTime } = AccessTokenController; const [isLoggedIn, setIsLoggedIn] = useState(hasAccessToken && hasTokenDateTime); - const { fetchUserInfo } = useUserInfo(); - - useEffect(() => { - if (isLoggedIn) fetchUserInfo(); - }, []); return {children}; }; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 06955b406..881e1d715 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -2,18 +2,14 @@ import { useContext } from 'react'; import AccessTokenController from '@auth/accessTokenController'; -import { useUserInfo } from '@hooks/useUserInfo'; - import { LoginContext } from '@context/login/LoginProvider'; export const useAuth = () => { const { isLoggedIn, setIsLoggedIn } = useContext(LoginContext); - const { fetchUserInfo } = useUserInfo(); const login = (accesssToken: string, expiredTime: number) => { AccessTokenController.save(accesssToken, expiredTime); setIsLoggedIn(true); - fetchUserInfo(); }; const logout = () => { diff --git a/frontend/src/hooks/useUserInfo.ts b/frontend/src/hooks/useUserInfo.ts index a6f57323d..54b4b312b 100644 --- a/frontend/src/hooks/useUserInfo.ts +++ b/frontend/src/hooks/useUserInfo.ts @@ -2,11 +2,25 @@ import { useContext, useEffect } from 'react'; import { useGetUserInformation } from '@api/member'; +import { useAuth } from '@hooks/useAuth'; + import { UserInfoContext } from '@context/userInfo/UserInfoProvider'; export const useUserInfo = () => { const { userInfo, setUserInfo } = useContext(UserInfoContext); - const { data, refetch: fetchUserInfo, isError, isSuccess } = useGetUserInformation(); + + const { isLoggedIn } = useAuth(); + + const { + data, + refetch: fetchUserInfo, + isError, + isSuccess, + } = useGetUserInformation({ + options: { + enabled: isLoggedIn, + }, + }); useEffect(() => { if (!data || isError || !isSuccess) return; diff --git a/frontend/src/hooks/useUserRole.ts b/frontend/src/hooks/useUserRole.ts index 9e9051652..b4c040960 100644 --- a/frontend/src/hooks/useUserRole.ts +++ b/frontend/src/hooks/useUserRole.ts @@ -2,13 +2,30 @@ import { useContext, useEffect } from 'react'; import { USER_ROLE } from '@constants'; -import { type ApiUserRole, useGetUserRole } from '@api/member'; +import { StudyId } from '@custom-types'; + +import { useGetUserRole } from '@api/member'; + +import { useAuth } from '@hooks/useAuth'; import { UserRoleContext } from '@context/userRole/UserRoleProvider'; -export const useUserRole = ({ studyId, options }: ApiUserRole['get']['variables']) => { +export const useUserRole = ({ studyId }: { studyId: StudyId }) => { const { userRole, setUserRole } = useContext(UserRoleContext); - const { data, refetch: fetchUserRole, isError, isSuccess } = useGetUserRole({ studyId, options }); + + const { isLoggedIn } = useAuth(); + + const { + data, + refetch: fetchUserRole, + isError, + isSuccess, + } = useGetUserRole({ + studyId, + options: { + enabled: isLoggedIn, + }, + }); useEffect(() => { if (!data || isError || !isSuccess) return; diff --git a/frontend/src/pages/detail-page/hooks/useDetailPage.ts b/frontend/src/pages/detail-page/hooks/useDetailPage.ts index 7429a46cd..aa8f90dcc 100644 --- a/frontend/src/pages/detail-page/hooks/useDetailPage.ts +++ b/frontend/src/pages/detail-page/hooks/useDetailPage.ts @@ -15,12 +15,7 @@ const useDetailPage = () => { const detailQueryResult = useGetStudy({ studyId }); const { mutate } = usePostMyStudy(); - const { isOwner, isOwnerOrMember, fetchUserRole } = useUserRole({ - studyId, - options: { - enabled: isLoggedIn, - }, - }); + const { isOwner, isOwnerOrMember, fetchUserRole } = useUserRole({ studyId }); const handleRegisterButtonClick = () => { if (!isLoggedIn) { diff --git a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/article/Article.tsx b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/article/Article.tsx index fe4d84103..a20a56464 100644 --- a/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/article/Article.tsx +++ b/frontend/src/pages/study-room-page/tabs/community-tab-panel/components/article/Article.tsx @@ -7,7 +7,8 @@ import { changeDateSeperator } from '@utils'; import tw from '@utils/tw'; import { useDeleteCommunityArticle, useGetCommunityArticle } from '@api/community'; -import { useGetUserInformation } from '@api/member'; + +import { useUserInfo } from '@hooks/useUserInfo'; import { BoxButton } from '@components/button'; import ButtonGroup from '@components/button-group/ButtonGroup'; @@ -24,7 +25,7 @@ export type ArticleProps = { const Article: FC = ({ studyId, articleId }) => { const { isFetching, isSuccess, isError, data } = useGetCommunityArticle({ studyId, articleId }); - const getUserInformationQueryResult = useGetUserInformation(); + const { userInfo } = useUserInfo(); const { mutateAsync } = useDeleteCommunityArticle(); const navigate = useNavigate(); @@ -44,31 +45,6 @@ const Article: FC = ({ studyId, articleId }) => { ); }; - const renderModifierButtons = () => { - if (!getUserInformationQueryResult.isSuccess || getUserInformationQueryResult.isError) return; - if (!data?.author.username) return; - if (data.author.username !== getUserInformationQueryResult.data.username) return; - - return ( - - - - 글수정 - - - - 글삭제 - - - ); - }; - const render = () => { if (isFetching) { return
Loading...
; @@ -79,6 +55,8 @@ const Article: FC = ({ studyId, articleId }) => { if (isSuccess) { const { title, author, content, createdDate } = data; + const isMyArticle = author.id === userInfo.id; + return (
@@ -86,7 +64,24 @@ const Article: FC = ({ studyId, articleId }) => { {author.username} {changeDateSeperator(createdDate)} - {renderModifierButtons()} + {isMyArticle && ( + + + + 글수정 + + + + 글삭제 + + + )} {title} diff --git a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts index 1c518c5e8..9b0c15d78 100644 --- a/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts +++ b/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/hooks/useLinkRoomTabPanel.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { useGetInfiniteLinks } from '@api/links'; @@ -10,17 +10,13 @@ export const useLinkRoomTabPanel = () => { const { studyId: _studyId } = useParams<{ studyId: string }>(); const studyId = Number(_studyId); - const { fetchUserInfo, userInfo } = useUserInfo(); + const { userInfo } = useUserInfo(); const { isOwnerOrMember } = useUserRole({ studyId }); const infiniteLinksQueryResult = useGetInfiniteLinks({ studyId }); const [isModalOpen, setIsModalOpen] = useState(false); - useEffect(() => { - fetchUserInfo(); - }, []); - const handleLinkAddButtonClick = () => setIsModalOpen(prev => !prev); const handleModalClose = () => setIsModalOpen(false); diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/article/Article.tsx b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/article/Article.tsx index 428c80ae1..9a604920d 100644 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/article/Article.tsx +++ b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/components/article/Article.tsx @@ -5,9 +5,10 @@ import { PATH } from '@constants'; import { changeDateSeperator } from '@utils'; import tw from '@utils/tw'; -import { useGetUserInformation } from '@api/member'; import { useDeleteNoticeArticle, useGetNoticeArticle } from '@api/notice'; +import { useUserInfo } from '@hooks/useUserInfo'; + import { BoxButton } from '@components/button'; import ButtonGroup from '@components/button-group/ButtonGroup'; import Divider from '@components/divider/Divider'; @@ -23,7 +24,7 @@ export type ArticleProps = { const Article: React.FC = ({ studyId, articleId }) => { const { isFetching, isSuccess, isError, data } = useGetNoticeArticle({ studyId, articleId }); - const getUserInformationQueryResult = useGetUserInformation(); + const { userInfo } = useUserInfo(); const { mutateAsync } = useDeleteNoticeArticle(); const navigate = useNavigate(); @@ -43,31 +44,6 @@ const Article: React.FC = ({ studyId, articleId }) => { ); }; - const renderModifierButtons = () => { - if (!getUserInformationQueryResult.isSuccess || getUserInformationQueryResult.isError) return; - if (!data?.author.username) return; - if (data.author.username !== getUserInformationQueryResult.data.username) return; - - return ( - - - - 글수정 - - - - 글삭제 - - - ); - }; - const render = () => { if (isFetching) { return
Loading...
; @@ -78,6 +54,8 @@ const Article: React.FC = ({ studyId, articleId }) => { if (isSuccess) { const { title, author, content, createdDate } = data; + const isMyArticle = author.id === userInfo.id; + return (
@@ -85,7 +63,24 @@ const Article: React.FC = ({ studyId, articleId }) => { {author.username} {changeDateSeperator(createdDate)} - {renderModifierButtons()} + {isMyArticle && ( + + + + 글수정 + + + + 글삭제 + + + )} {title} diff --git a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/hooks/usePermission.ts b/frontend/src/pages/study-room-page/tabs/notice-tab-panel/hooks/usePermission.ts deleted file mode 100644 index a63a59b30..000000000 --- a/frontend/src/pages/study-room-page/tabs/notice-tab-panel/hooks/usePermission.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { UserRole } from '@custom-types'; - -import { useGetUserRole } from '@api/member'; - -const usePermission = (studyId: string | number, allowedRole: UserRole) => { - const { data, isFetching, isSuccess, isError } = useGetUserRole({ studyId: Number(studyId) }); - const hasPermission = isSuccess && !isError && data.role === allowedRole; - - return { - isFetching, - isError, - hasPermission, - }; -}; - -export default usePermission; diff --git a/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx b/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx index 570687856..7d7ceee26 100644 --- a/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx +++ b/frontend/src/pages/study-room-page/tabs/review-tab-panel/ReviewTabPanel.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { useGetStudyReviews } from '@api/reviews'; @@ -16,15 +15,11 @@ const ReviewTabPanel: React.FC = () => { const { studyId: _studyId } = useParams<{ studyId: string }>(); const studyId = Number(_studyId); - const { userInfo, fetchUserInfo } = useUserInfo(); + const { userInfo } = useUserInfo(); const { isOwnerOrMember } = useUserRole({ studyId }); const { data, isFetching, refetch, isError, isSuccess } = useGetStudyReviews({ studyId }); - useEffect(() => { - fetchUserInfo(); - }, []); - const handlePostSuccess = () => { alert('댓글을 추가했습니다'); refetch(); From efe168e4e3f3cd923dfb0da036db883822fa6051 Mon Sep 17 00:00:00 2001 From: TaeYoon Date: Sun, 16 Oct 2022 17:34:24 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[FE]=20fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=84=B1=EA=B3=B5=20=ED=9B=84=20=ED=97=A4=EB=8D=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=94=EB=80=8C=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 로그인시 헤더가 바뀌지 않는 문제 해결 * refactor: eslint 적용 --- frontend/src/layout/header/Header.tsx | 10 ++++++++-- .../login-redirect-page/hooks/useLoginRedirectPage.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/layout/header/Header.tsx b/frontend/src/layout/header/Header.tsx index fb988098e..76d3f52f7 100644 --- a/frontend/src/layout/header/Header.tsx +++ b/frontend/src/layout/header/Header.tsx @@ -1,5 +1,5 @@ import { useContext, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { PATH } from '@constants'; @@ -27,6 +27,8 @@ const Header: React.FC = () => { const [isOpenDropDownBox, setIsOpenDropDownBox] = useState(false); const navigate = useNavigate(); + const location = useLocation(); + const { logout, isLoggedIn } = useAuth(); const { userInfo } = useUserInfo(); @@ -41,6 +43,10 @@ const Header: React.FC = () => { navigate(PATH.MAIN); }; + const handleLoginButtonClick = () => { + window.sessionStorage.setItem('prevPath', location.pathname); + }; + const handleLogoutButtonClick = () => { logout(); }; @@ -94,7 +100,7 @@ const Header: React.FC = () => { ) : ( - + Github 로그인 diff --git a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts index 49dd78526..94ebdb7a2 100644 --- a/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts +++ b/frontend/src/pages/login-redirect-page/hooks/useLoginRedirectPage.ts @@ -10,6 +10,7 @@ import { useAuth } from '@hooks/useAuth'; const useLoginRedirectPage = () => { const [searchParams] = useSearchParams(); const codeParam = searchParams.get('code'); + const navigate = useNavigate(); const { login } = useAuth(); @@ -23,16 +24,19 @@ const useLoginRedirectPage = () => { return; } + // 외부 사이트에서 리다이렉트하는 것이기 때문에 react-router의 location state를 사용할 수 없어 sessionStorage를 이용 + const prevPath = window.sessionStorage.getItem('prevPath') || PATH.MAIN; + mutate( { code: codeParam }, { onError: () => { alert('로그인에 실패했습니다.'); - navigate(PATH.MAIN, { replace: true }); + navigate(prevPath, { replace: true }); }, onSuccess: ({ accessToken, expiredTime }) => { login(accessToken, expiredTime); - navigate(-1); + navigate(prevPath, { replace: true }); }, }, );