From 6298f48d412641ba21dcb78cac97186a57000977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Mon, 21 Aug 2023 23:24:08 +0900 Subject: [PATCH 01/53] =?UTF-8?q?chore:=20develop=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=EC=97=90=20=EB=B9=84=ED=95=B4=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EC=BB=A4=EB=B0=8B=20=EB=B0=98=EC=98=81=20(#364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/fe-pull-request.yml | 16 +++++++++++++--- frontend/cypress/e2e/mapbefine.cy.ts | 22 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fe-pull-request.yml b/.github/workflows/fe-pull-request.yml index 25e16481..69d681f0 100644 --- a/.github/workflows/fe-pull-request.yml +++ b/.github/workflows/fe-pull-request.yml @@ -1,13 +1,15 @@ name: Frontend CI For Test Validation -# 어떤 이벤트가 발생하면 실행할지 결정 + on: - #pull request open과 reopen 시 실행한다. + # pull request open과 reopen 시 실행한다. pull_request: branches: [main, develop] paths: frontend/** + defaults: run: working-directory: ./frontend + jobs: jest: runs-on: ubuntu-22.04 @@ -19,10 +21,18 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: "18" + node-version: '18' - name: Install node modules run: npm install - name: Run Jest test run: npm run test + + - name: Start npm + run: npm run dev + + - name: Run Cypress + uses: cypress-io/github-action@v5 + with: + start: npm run cypress diff --git a/frontend/cypress/e2e/mapbefine.cy.ts b/frontend/cypress/e2e/mapbefine.cy.ts index cb5a7452..f24052ad 100644 --- a/frontend/cypress/e2e/mapbefine.cy.ts +++ b/frontend/cypress/e2e/mapbefine.cy.ts @@ -69,12 +69,30 @@ describe('토픽 상세 페이지', () => { if (index === 0) $el.click(); }); - cy.get('li') + cy.wait(1000); + + cy.get('span').each(($el, index) => { + if (index === 6) $el.click(); + }); + + cy.contains('내 지도에 저장하기').should('be.visible'); + }); + + it('핀 상세 페이지에서 내 지도에 저장하기 버튼 누르면 토스트 메시지가 나온다.', () => { + cy.get('[data-cy="topic-card"]') .children() .each(($el, index) => { if (index === 0) $el.click(); }); - cy.contains('내 지도에 저장하기').should('be.visible'); + cy.wait(1000); + + cy.get('span').each(($el, index) => { + if (index === 6) $el.click(); + }); + + cy.contains('내 지도에 저장하기').click(); + + cy.contains('로그인 후 사용해주세요.').should('be.visible'); }); }); From a8cf6221dcdda3ad45c04bc9d16528d32e99faf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:43:26 +0900 Subject: [PATCH 02/53] =?UTF-8?q?[FE]=20Refactor/#365=20API=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?(#368)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename: 2 Depth 컴포넌트 디렉토리 구조 변경 * refactor: 서버와 지도 GET API 요청 로직 분리 * refactor: 서버와 로그인 GET API 분리 * refactor: api 요청 로직 에러 핸들링 추가 및 명시적으로 조건문 지정 추가로 content-type을 내로잉한다. * fix: 누락된 api 수정 적용 * rename: 사용하지 않는 svg 제거 및 오타 수정 * refactor: 타입명 코드 컨벤션에 맞게 모두 변경 * refactor: api get 요청 로직 커스텀 훅으로 분리 * feat: usePost api 요청 커스텀 훅 생성 * feat: usePut api 요청 커스텀 훅 생성 * feat: useDelete api 요청 커스텀 훅 생성 * remove: 사용하지 않는 페이지 컴포넌트 제거 * fix: delete 요청을 두 번씩 날리는 오류 수정 * refactor: Home 컴포넌트 뎁스 줄임 및 fetchGet 적용 * remove: 불필요한 페이지 컴포넌트 제거 * refactor: bookmark 페이지 및 관련 컴포넌트 관심사 분리 * rename: 불필요한 페이지 제거 및 페이지명 변경 * refactor: 누락된 타입 프로퍼티 추가 * fix: 유효하지 않은 주소를 클릭 시 에러 토스트를 띄우는 기능 수정 * rename: Topic Card 컴포넌트를 담는 상위 컨테이너 컴포넌트 명 변경 * design: myInfo의 사용자 성명 font weight 수정 * style: 불필요한 import 제거 및 공백 수정 * refactor: 중복되는 역할의 컴포넌트 제거 후 재사용 * refactor: 잘못된 에러 메시지 수정 * design: 내가 만든 지도가 없을 경우 사용자에게 알림 문구 더 친화적으로 수정 * refactor: 중복 컴포넌트 제거 및 TopicCardList 재사용 반영 * refactor: 10m 이내로 핀이 찍힐 확률을 줄이면서 사용성을 개선할 수 있는 줌 Limit 수정 * rename: 스켈레톤 컴포넌트 불규칙했던 디렉토리 구조 통일 * rename: NotFound 페이지 컴포넌트 디렉토리 위치 조정 * refactor: usePost를 success message 대신 onSuccess 함수 받도록 변경 * refactor: 모달용 토픽 카드 컴포넌트 제거 후 기존 토픽카드에 타입 프롭 추가 추가로 api 요청 커스텀 훅을 적용한다. * refactor: put, delete api 요청 hook interface 조정 --- frontend/src/apiHooks/useDelete.ts | 32 ++++ frontend/src/apiHooks/useGet.ts | 23 +++ frontend/src/apiHooks/usePost.ts | 35 ++++ frontend/src/apiHooks/usePut.ts | 35 ++++ frontend/src/apis/deleteApi.ts | 33 +++- frontend/src/apis/getApi.ts | 24 +-- frontend/src/apis/getLoginApi.ts | 16 ++ frontend/src/apis/getMapApi.ts | 23 +-- frontend/src/apis/postApi.ts | 29 +++- frontend/src/apis/putApi.ts | 23 ++- frontend/src/assets/ModifyMyInfoIcon.svg | 3 - frontend/src/assets/My.svg | 6 - ...efalutImg.svg => profile_defaultImage.svg} | 0 frontend/src/components/AddFavorite/index.tsx | 14 +- .../src/components/AddSeeTogether/index.tsx | 14 +- .../src/components/BookmarksList/index.tsx | 45 ----- frontend/src/components/Layout/index.tsx | 2 +- frontend/src/components/Loader/index.tsx | 2 +- .../ModalMyTopicList/addToMyTopicList.tsx | 90 ++++++---- .../src/components/ModalMyTopicList/index.tsx | 53 +++--- .../src/components/ModalTopicCard/index.tsx | 125 -------------- .../src/components/MyInfo/UpdateMyInfo.tsx | 17 +- frontend/src/components/MyInfo/index.tsx | 6 +- .../MyInfoContainer/MyInfoList/index.tsx | 62 ------- .../src/components/MyInfoContainer/index.tsx | 56 ------- frontend/src/components/PinsOfTopic/index.tsx | 4 +- .../SeeAllCardList/SeeAllCardListSkeleton.tsx | 19 --- .../src/components/SeeAllCardList/index.tsx | 53 ------ .../components/SeeTogetherCounter/index.tsx | 4 +- .../PinPreviewSkeleton.tsx} | 0 .../PinsOfTopicSkeleton.tsx | 4 +- .../TopicCardSkeleton.tsx} | 0 .../TopicInfoSkeleton.tsx} | 0 .../TopicListSkeleton.tsx} | 6 +- frontend/src/components/TopicCard/index.tsx | 67 +++++--- .../components/TopicCardContainer/index.tsx | 122 ++++++++++++++ .../src/components/TopicCardList/index.tsx | 124 +++++++++----- .../components/TopicListContainer/index.tsx | 83 ---------- frontend/src/context/SeeTogetherContext.tsx | 8 +- frontend/src/hooks/useMapClick.ts | 18 +- frontend/src/index.tsx | 2 +- frontend/src/lib/getAddressFromServer.ts | 10 +- .../pages/{LoginError.tsx => AskLogin.tsx} | 4 +- frontend/src/pages/AskLoginPage.tsx | 61 ------- frontend/src/pages/Bookmark.tsx | 139 +++++----------- frontend/src/pages/ErrorPage.tsx | 62 ------- frontend/src/pages/Home.tsx | 156 +++++------------- frontend/src/pages/KaKaoRedirectPage.tsx | 4 +- frontend/src/pages/NewPin.tsx | 25 ++- frontend/src/pages/NewTopic.tsx | 8 +- .../NotFound/index.tsx => pages/NotFound.tsx} | 12 +- frontend/src/pages/PinDetail.tsx | 12 +- frontend/src/pages/Profile.tsx | 62 +++++-- frontend/src/pages/SeeAllLatestTopics.tsx | 26 ++- frontend/src/pages/SeeAllNearTopics.tsx | 26 ++- frontend/src/pages/SeeAllPopularTopics.tsx | 26 ++- frontend/src/pages/SeeAllTopics.tsx | 25 --- frontend/src/pages/SeeTogetherTopics.tsx | 11 +- frontend/src/pages/SelectedTopic.tsx | 21 +-- frontend/src/router.tsx | 8 +- frontend/src/types/Api.ts | 1 + frontend/src/types/Bookmarks.ts | 2 +- frontend/src/types/Login.ts | 6 +- frontend/src/types/Map.ts | 65 ++++++++ frontend/src/types/MyInfo.ts | 23 --- frontend/src/types/Pin.ts | 3 +- frontend/src/types/Profile.ts | 4 + frontend/src/types/Topic.ts | 19 ++- 68 files changed, 944 insertions(+), 1159 deletions(-) create mode 100644 frontend/src/apiHooks/useDelete.ts create mode 100644 frontend/src/apiHooks/useGet.ts create mode 100644 frontend/src/apiHooks/usePost.ts create mode 100644 frontend/src/apiHooks/usePut.ts create mode 100644 frontend/src/apis/getLoginApi.ts delete mode 100644 frontend/src/assets/ModifyMyInfoIcon.svg delete mode 100644 frontend/src/assets/My.svg rename frontend/src/assets/{InfoDefalutImg.svg => profile_defaultImage.svg} (100%) delete mode 100644 frontend/src/components/BookmarksList/index.tsx delete mode 100644 frontend/src/components/ModalTopicCard/index.tsx delete mode 100644 frontend/src/components/MyInfoContainer/MyInfoList/index.tsx delete mode 100644 frontend/src/components/MyInfoContainer/index.tsx delete mode 100644 frontend/src/components/SeeAllCardList/SeeAllCardListSkeleton.tsx delete mode 100644 frontend/src/components/SeeAllCardList/index.tsx rename frontend/src/components/{PinPreviewSkeleton/index.tsx => Skeletons/PinPreviewSkeleton.tsx} (100%) rename frontend/src/components/{PinsOfTopic => Skeletons}/PinsOfTopicSkeleton.tsx (79%) rename frontend/src/components/{TopicCardSkeleton/index.tsx => Skeletons/TopicCardSkeleton.tsx} (100%) rename frontend/src/components/{TopicInfoSkeleton/index.tsx => Skeletons/TopicInfoSkeleton.tsx} (100%) rename frontend/src/components/{TopicCardList/TopicCardListSkeleton.tsx => Skeletons/TopicListSkeleton.tsx} (73%) create mode 100644 frontend/src/components/TopicCardContainer/index.tsx delete mode 100644 frontend/src/components/TopicListContainer/index.tsx rename frontend/src/pages/{LoginError.tsx => AskLogin.tsx} (96%) delete mode 100644 frontend/src/pages/AskLoginPage.tsx delete mode 100644 frontend/src/pages/ErrorPage.tsx rename frontend/src/{components/NotFound/index.tsx => pages/NotFound.tsx} (82%) delete mode 100644 frontend/src/pages/SeeAllTopics.tsx create mode 100644 frontend/src/types/Api.ts create mode 100644 frontend/src/types/Map.ts delete mode 100644 frontend/src/types/MyInfo.ts create mode 100644 frontend/src/types/Profile.ts diff --git a/frontend/src/apiHooks/useDelete.ts b/frontend/src/apiHooks/useDelete.ts new file mode 100644 index 00000000..38f61d67 --- /dev/null +++ b/frontend/src/apiHooks/useDelete.ts @@ -0,0 +1,32 @@ +import { deleteApi } from '../apis/deleteApi'; +import useToast from '../hooks/useToast'; +import { ContentTypeType } from '../types/Api'; + +interface fetchDeleteProps { + url: string; + contentType?: ContentTypeType; +} + +const useDelete = () => { + const { showToast } = useToast(); + + const fetchDelete = async ( + { url, contentType }: fetchDeleteProps, + errorMessage: string, + onSuccess: () => void, + ) => { + try { + await deleteApi(url, contentType); + + if (onSuccess) { + onSuccess(); + } + } catch (e) { + showToast('error', errorMessage); + } + }; + + return { fetchDelete }; +}; + +export default useDelete; diff --git a/frontend/src/apiHooks/useGet.ts b/frontend/src/apiHooks/useGet.ts new file mode 100644 index 00000000..f55b4cc0 --- /dev/null +++ b/frontend/src/apiHooks/useGet.ts @@ -0,0 +1,23 @@ +import { getApi } from '../apis/getApi'; +import useToast from '../hooks/useToast'; + +const useGet = () => { + const { showToast } = useToast(); + + const fetchGet = async ( + url: string, + errorMessage: string, + onSuccess: (responseData: T) => void, + ) => { + try { + const responseData = await getApi(url); + onSuccess(responseData); + } catch (e) { + showToast('error', errorMessage); + } + }; + + return { fetchGet }; +}; + +export default useGet; diff --git a/frontend/src/apiHooks/usePost.ts b/frontend/src/apiHooks/usePost.ts new file mode 100644 index 00000000..df63216a --- /dev/null +++ b/frontend/src/apiHooks/usePost.ts @@ -0,0 +1,35 @@ +import { postApi } from '../apis/postApi'; +import useToast from '../hooks/useToast'; +import { ContentTypeType } from '../types/Api'; + +interface fetchPostProps { + url: string; + payload: {}; + contentType?: ContentTypeType; +} + +const usePost = () => { + const { showToast } = useToast(); + + const fetchPost = async ( + { url, payload, contentType }: fetchPostProps, + errorMessage: string, + onSuccess?: () => void, + ) => { + try { + const responseData = await postApi(url, payload, contentType); + + if (onSuccess) { + onSuccess(); + } + + return responseData; + } catch (e) { + showToast('error', errorMessage); + } + }; + + return { fetchPost }; +}; + +export default usePost; diff --git a/frontend/src/apiHooks/usePut.ts b/frontend/src/apiHooks/usePut.ts new file mode 100644 index 00000000..5316a6b6 --- /dev/null +++ b/frontend/src/apiHooks/usePut.ts @@ -0,0 +1,35 @@ +import { putApi } from '../apis/putApi'; +import useToast from '../hooks/useToast'; +import { ContentTypeType } from '../types/Api'; + +interface fetchPutProps { + url: string; + payload: {}; + contentType?: ContentTypeType; +} + +const usePut = () => { + const { showToast } = useToast(); + + const fetchPut = async ( + { url, payload, contentType }: fetchPutProps, + errorMessage: string, + onSuccess?: () => void, + ) => { + try { + const responseData = await putApi(url, payload, contentType); + + if (onSuccess) { + onSuccess(); + } + + return responseData; + } catch (e) { + showToast('error', errorMessage); + } + }; + + return { fetchPut }; +}; + +export default usePut; diff --git a/frontend/src/apis/deleteApi.ts b/frontend/src/apis/deleteApi.ts index 0cf943fb..7707d046 100644 --- a/frontend/src/apis/deleteApi.ts +++ b/frontend/src/apis/deleteApi.ts @@ -4,13 +4,34 @@ // : process.env.REACT_APP_API_DEFAULT_DEV; import { DEFAULT_PROD_URL } from '../constants'; +import { ContentTypeType } from '../types/Api'; -export const deleteApi = async (url: string, contentType?: string) => { - await fetch(`${DEFAULT_PROD_URL + url}`, { +interface Headers { + 'content-type': string; + [key: string]: string; +} + +export const deleteApi = async (url: string, contentType?: ContentTypeType) => { + const apiUrl = `${DEFAULT_PROD_URL + url}`; + const userToken = localStorage.getItem('userToken'); + const headers: Headers = { + 'content-type': 'application/json', + }; + + if (userToken) { + headers['Authorization'] = `Bearer ${userToken}`; + } + + if (contentType) { + headers['content-type'] = contentType; + } + + const response = await fetch(apiUrl, { method: 'DELETE', - headers: { - Authorization: `Bearer ${localStorage.getItem('userToken') || ''}`, - 'Content-Type': contentType || 'application/json', - }, + headers, }); + + if (response.status >= 400) { + throw new Error('[SERVER] DELETE 요청에 실패했습니다.'); + } }; diff --git a/frontend/src/apis/getApi.ts b/frontend/src/apis/getApi.ts index 804e138e..08df249b 100644 --- a/frontend/src/apis/getApi.ts +++ b/frontend/src/apis/getApi.ts @@ -6,31 +6,31 @@ import { DEFAULT_PROD_URL } from '../constants'; interface Headers { - 'Content-Type': string; + 'content-type': string; [key: string]: string; } -export const getApi = async ( - type: 'tMap' | 'default' | 'login', - url: string, -): Promise => { - const apiUrl = - type === 'tMap' || type === 'login' ? url : `${DEFAULT_PROD_URL + url}`; +export const getApi = async (url: string) => { + const apiUrl = `${DEFAULT_PROD_URL + url}`; const userToken = localStorage.getItem('userToken'); const headers: Headers = { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }; + if (userToken) { headers['Authorization'] = `Bearer ${userToken}`; } + const response = await fetch(apiUrl, { method: 'GET', - headers: headers, + headers, }); - const responseData: T = await response.json(); + if (response.status >= 400) { - //todo: status 상태별로 로그인 토큰 유효 검증 - throw new Error('API 요청에 실패했습니다.'); + throw new Error('[SERVER] GET 요청에 실패했습니다.'); } + + const responseData: T = await response.json(); + return responseData; }; diff --git a/frontend/src/apis/getLoginApi.ts b/frontend/src/apis/getLoginApi.ts new file mode 100644 index 00000000..2466e95a --- /dev/null +++ b/frontend/src/apis/getLoginApi.ts @@ -0,0 +1,16 @@ +export const getLoginApi = async (url: string) => { + const response = await fetch(url, { + method: 'GET', + headers: { + 'content-type': 'application/json', + }, + }); + + if (response.status >= 400) { + throw new Error('[KAKAO] GET 요청에 실패했습니다.'); + } + + const responseData: T = await response.json(); + + return responseData; +}; diff --git a/frontend/src/apis/getMapApi.ts b/frontend/src/apis/getMapApi.ts index 5e552b9d..c6883278 100644 --- a/frontend/src/apis/getMapApi.ts +++ b/frontend/src/apis/getMapApi.ts @@ -1,13 +1,16 @@ -export const getMapApi = (url: string) => - fetch(url, { +export const getMapApi = async (url: string) => { + const response = await fetch(url, { method: 'GET', headers: { - 'Content-type': 'application/json', + 'content-type': 'application/json', }, - }) - .then((data) => { - return data.json(); - }) - .catch((error) => { - throw new Error(`${error.message}`); - }); + }); + + if (response.status >= 400) { + throw new Error('[MAP] GET 요청에 실패했습니다.'); + } + + const responseData: T = await response.json(); + + return responseData; +}; diff --git a/frontend/src/apis/postApi.ts b/frontend/src/apis/postApi.ts index 0b4284b8..44009be9 100644 --- a/frontend/src/apis/postApi.ts +++ b/frontend/src/apis/postApi.ts @@ -4,28 +4,41 @@ // : process.env.REACT_APP_API_DEFAULT_DEV; import { DEFAULT_PROD_URL } from '../constants'; +import { ContentTypeType } from '../types/Api'; interface Headers { - 'Content-Type': string; + 'content-type': string; [key: string]: string; } -export const postApi = async (url: string, data?: {}, contentType?: string) => { + +export const postApi = async ( + url: string, + payload: {}, + contentType?: ContentTypeType, +) => { + const apiUrl = `${DEFAULT_PROD_URL + url}`; const userToken = localStorage.getItem('userToken'); const headers: Headers = { - 'Content-Type': `${contentType || 'application/json'}`, + 'content-type': 'application/json', }; + if (userToken) { headers['Authorization'] = `Bearer ${userToken}`; } - const response = await fetch(`${DEFAULT_PROD_URL + url}`, { + if (contentType) { + headers['content-type'] = contentType; + } + + const response = await fetch(apiUrl, { method: 'POST', - headers: headers, - body: JSON.stringify(data), + headers, + body: JSON.stringify(payload), }); + if (response.status >= 400) { - //todo: status 상태별로 로그인 토큰 유효 검증 - throw new Error('API 요청에 실패했습니다.'); + throw new Error('[SERVER] POST 요청에 실패했습니다.'); } + return response; }; diff --git a/frontend/src/apis/putApi.ts b/frontend/src/apis/putApi.ts index 5fffb872..13d87f61 100644 --- a/frontend/src/apis/putApi.ts +++ b/frontend/src/apis/putApi.ts @@ -4,30 +4,41 @@ // : process.env.REACT_APP_API_DEFAULT_DEV; import { DEFAULT_PROD_URL } from '../constants'; +import { ContentTypeType } from '../types/Api'; interface Headers { - 'Content-Type': string; + 'content-type': string; [key: string]: string; } + export const putApi = async ( url: string, - data: { name: string; images: string[]; description: string }, + data: {}, + contentType?: ContentTypeType, ) => { + const apiUrl = `${DEFAULT_PROD_URL + url}`; const userToken = localStorage.getItem('userToken'); const headers: Headers = { - 'Content-Type': 'application/json', + 'content-type': 'application/json', }; + if (userToken) { headers['Authorization'] = `Bearer ${userToken}`; } - const response = await fetch(`${DEFAULT_PROD_URL + url}`, { + if (contentType) { + headers['content-type'] = contentType; + } + + const response = await fetch(apiUrl, { method: 'PUT', - headers: headers, + headers, body: JSON.stringify(data), }); + if (response.status >= 400) { - throw new Error('API 요청에 실패했습니다.'); + throw new Error('[SERVER] PUT 요청에 실패했습니다.'); } + return response; }; diff --git a/frontend/src/assets/ModifyMyInfoIcon.svg b/frontend/src/assets/ModifyMyInfoIcon.svg deleted file mode 100644 index dbc9b1b9..00000000 --- a/frontend/src/assets/ModifyMyInfoIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/src/assets/My.svg b/frontend/src/assets/My.svg deleted file mode 100644 index 509ca1a2..00000000 --- a/frontend/src/assets/My.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/src/assets/InfoDefalutImg.svg b/frontend/src/assets/profile_defaultImage.svg similarity index 100% rename from frontend/src/assets/InfoDefalutImg.svg rename to frontend/src/assets/profile_defaultImage.svg diff --git a/frontend/src/components/AddFavorite/index.tsx b/frontend/src/components/AddFavorite/index.tsx index 5fc7dc60..fe46e1c3 100644 --- a/frontend/src/components/AddFavorite/index.tsx +++ b/frontend/src/components/AddFavorite/index.tsx @@ -6,14 +6,14 @@ import { deleteApi } from '../../apis/deleteApi'; interface AddFavoriteProps { id: number; isBookmarked: boolean; - setTopicsFromServer: () => void; + getTopicsFromServer: () => void; children: React.ReactNode; } const AddFavorite = ({ id, isBookmarked, - setTopicsFromServer, + getTopicsFromServer, children, }: AddFavoriteProps) => { const { showToast } = useToast(); @@ -24,7 +24,7 @@ const AddFavorite = ({ try { await postApi(`/bookmarks/topics?id=${id}`, {}, 'x-www-form-urlencoded'); - setTopicsFromServer(); + getTopicsFromServer(); showToast('info', '즐겨찾기에 추가되었습니다.'); } catch { @@ -38,18 +38,12 @@ const AddFavorite = ({ try { await deleteApi(`/bookmarks/topics?id=${id}`, 'x-www-form-urlencoded'); - setTopicsFromServer(); + getTopicsFromServer(); showToast('info', '해당 지도를 즐겨찾기에서 제외했습니다.'); } catch { showToast('error', '로그인 후 사용해주세요.'); } - - await deleteApi(`/bookmarks/topics?id=${id}`, 'x-www-form-urlencoded'); - - setTopicsFromServer(); - - showToast('info', '해당 지도를 즐겨찾기에서 제외했습니다.'); }; return ( diff --git a/frontend/src/components/AddSeeTogether/index.tsx b/frontend/src/components/AddSeeTogether/index.tsx index 401c3e40..9acfed43 100644 --- a/frontend/src/components/AddSeeTogether/index.tsx +++ b/frontend/src/components/AddSeeTogether/index.tsx @@ -3,7 +3,7 @@ import { postApi } from '../../apis/postApi'; import useToast from '../../hooks/useToast'; import { useContext } from 'react'; import { getApi } from '../../apis/getApi'; -import { TopicType } from '../../types/Topic'; +import { TopicCardProps } from '../../types/Topic'; import { SeeTogetherContext } from '../../context/SeeTogetherContext'; import { deleteApi } from '../../apis/deleteApi'; @@ -11,14 +11,14 @@ interface AddSeeTogetherProps { isInAtlas: boolean; id: number; children: React.ReactNode; - setTopicsFromServer: () => void; + getTopicsFromServer: () => void; } const AddSeeTogether = ({ isInAtlas, id, children, - setTopicsFromServer, + getTopicsFromServer, }: AddSeeTogetherProps) => { const { showToast } = useToast(); const { seeTogetherTopics, setSeeTogetherTopics } = @@ -35,12 +35,12 @@ const AddSeeTogether = ({ await postApi(`/atlas/topics?id=${id}`, {}, 'x-www-form-urlencoded'); - const topics = await getApi('default', '/members/my/atlas'); + const topics = await getApi('/members/my/atlas'); setSeeTogetherTopics(topics); // TODO: 모아보기 페이지에서는 GET /members/my/atlas 두 번 됨 - setTopicsFromServer(); + getTopicsFromServer(); showToast('info', '모아보기에 추가했습니다.'); } catch { @@ -54,12 +54,12 @@ const AddSeeTogether = ({ try { await deleteApi(`/atlas/topics?id=${id}`, 'x-www-form-urlencoded'); - const topics = await getApi('default', '/members/my/atlas'); + const topics = await getApi('/members/my/atlas'); setSeeTogetherTopics(topics); // TODO: 모아보기 페이지에서는 GET /members/my/atlas 두 번 됨 - setTopicsFromServer(); + getTopicsFromServer(); showToast('info', '해당 지도를 모아보기에서 제외했습니다.'); } catch { diff --git a/frontend/src/components/BookmarksList/index.tsx b/frontend/src/components/BookmarksList/index.tsx deleted file mode 100644 index aea2420c..00000000 --- a/frontend/src/components/BookmarksList/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Fragment, useEffect, useState } from 'react'; -import { styled } from 'styled-components'; -import { getApi } from '../../apis/getApi'; -import TopicCard from '../TopicCard'; -import { TopicType } from '../../types/Topic'; -import useToast from '../../hooks/useToast'; - -interface BookmarksListProps { - bookmarks: TopicType[]; - setTopicsFromServer: () => void; -} - -const BookmarksList = ({ - bookmarks, - setTopicsFromServer, -}: BookmarksListProps) => { - return ( - - {bookmarks.map((topic) => ( - - - - ))} - - ); -}; - -const Wrapper = styled.ul` - display: flex; - flex-wrap: wrap; - gap: 20px; -`; - -export default BookmarksList; diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index 5e5d36fb..666ca050 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -44,7 +44,7 @@ const Layout = ({ children }: LayoutProps) => { const map = new Tmapv3.Map(mapContainer.current, { center: new Tmapv3.LatLng(37.5154, 127.1029), }); - map.setZoomLimit(7, 17); + map.setZoomLimit(7, 18); setMap(map); return () => { map.destroy(); diff --git a/frontend/src/components/Loader/index.tsx b/frontend/src/components/Loader/index.tsx index 90f7ed3c..43066893 100644 --- a/frontend/src/components/Loader/index.tsx +++ b/frontend/src/components/Loader/index.tsx @@ -9,7 +9,7 @@ const Loader = () => { }; const Rotate = keyframes` - 0% { + 0% { transform: rotate(0deg); } 100% { diff --git a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx index d9f1ed80..d3ed6c4f 100644 --- a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx +++ b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx @@ -1,61 +1,81 @@ import { Fragment, useContext, useEffect, useState } from 'react'; -import { ModalMyTopicType } from '../../types/Topic'; -import { getApi } from '../../apis/getApi'; +import { TopicCardProps } from '../../types/Topic'; import { styled } from 'styled-components'; -import ModalTopicCard from '../ModalTopicCard'; +import TopicCard from '../TopicCard'; import { ModalContext } from '../../context/ModalContext'; -import { postApi } from '../../apis/postApi'; import useToast from '../../hooks/useToast'; +import useGet from '../../apiHooks/useGet'; +import usePost from '../../apiHooks/usePost'; + +interface OnClickDesignatedProps { + topicId: number; + topicName: string; +} const AddToMyTopicList = ({ pin }: any) => { - const [myTopics, setMyTopics] = useState([]); + const [myTopics, setMyTopics] = useState(null); const { closeModal } = useContext(ModalContext); + const { fetchGet } = useGet(); + const { fetchPost } = usePost(); const { showToast } = useToast(); - const getMyTopicFromServer = async () => { - const serverMyTopic = await getApi( - 'default', + const getMyTopicsFromServer = async () => { + fetchGet( '/members/my/topics', + '내가 만든 지도를 가져오는데 실패했습니다. 잠시 후 다시 시도해주세요.', + (response) => { + setMyTopics(response); + }, ); - setMyTopics(serverMyTopic); }; useEffect(() => { - getMyTopicFromServer(); + getMyTopicsFromServer(); }, []); - const addPinToTopic = async (topicId: any) => { - try { - await postApi(`/pins`, { - topicId: topicId.topicId, - name: pin.name, - description: pin.description, - address: pin.address, - latitude: pin.latitude, - longitude: pin.longitude, - legalDongCode: '', - }); - closeModal('addToMyTopicList'); - showToast('info', '내 지도에 핀이 추가되었습니다.'); - } catch (error) { - showToast('error', '내 지도에 핀 추가를 실패했습니다.'); - } + const addPinToTopic = async (topic: OnClickDesignatedProps) => { + const url = '/pins'; + const payload = { + topicId: topic.topicId, + name: pin.name, + description: pin.description, + address: pin.address, + latitude: pin.latitude, + longitude: pin.longitude, + legalDongCode: '', + }; + + fetchPost( + { + url, + payload, + }, + '내 지도에 핀 추가를 실패하였습니다. 잠시 후 다시 시도해주세요.', + () => { + closeModal('addToMyTopicList'); + showToast('info', '내 지도에 핀이 추가되었습니다.'); + }, + ); }; + if (!myTopics) return <>; return ( {myTopics.map((topic) => ( - ))} diff --git a/frontend/src/components/ModalMyTopicList/index.tsx b/frontend/src/components/ModalMyTopicList/index.tsx index a5e90e91..79981091 100644 --- a/frontend/src/components/ModalMyTopicList/index.tsx +++ b/frontend/src/components/ModalMyTopicList/index.tsx @@ -1,33 +1,39 @@ -import React, { Fragment, useEffect, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { styled } from 'styled-components'; -import { getApi } from '../../apis/getApi'; -import { ModalMyTopicType } from '../../types/Topic'; -import ModalTopicCard from '../ModalTopicCard'; +import { TopicCardProps } from '../../types/Topic'; +import TopicCard from '../TopicCard'; import Space from '../common/Space'; +import useGet from '../../apiHooks/useGet'; -interface ModalMyTopicList { +interface ModalMyTopicListProps { topicId: string; topicClick: any; } -const ModalMyTopicList = ({ topicId, topicClick }: ModalMyTopicList) => { - const [myTopics, setMyTopics] = useState([]); +const ModalMyTopicList = ({ topicId, topicClick }: ModalMyTopicListProps) => { + const [myTopics, setMyTopics] = useState(null); + const { fetchGet } = useGet(); const getMyTopicFromServer = async () => { if (topicId && topicId.split(',').length > 1) { - const topics = await getApi( - 'default', + fetchGet( `/topics/ids?ids=${topicId}`, + '모아보기로 선택한 지도 목록을 조회하는데 실패했습니다.', + (response) => { + setMyTopics(response); + }, ); - setMyTopics(topics); return; } - const serverMyTopic = await getApi( - 'default', + + fetchGet( '/members/my/topics', + '나의 지도 목록을 조회하는데 실패했습니다.', + (response) => { + setMyTopics(response); + }, ); - setMyTopics(serverMyTopic); }; useEffect(() => { @@ -41,15 +47,18 @@ const ModalMyTopicList = ({ topicId, topicClick }: ModalMyTopicList) => { {myTopics.map((topic) => ( - ))} diff --git a/frontend/src/components/ModalTopicCard/index.tsx b/frontend/src/components/ModalTopicCard/index.tsx deleted file mode 100644 index 9bd69333..00000000 --- a/frontend/src/components/ModalTopicCard/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { styled } from 'styled-components'; -import Text from '../common/Text'; -import useNavigator from '../../hooks/useNavigator'; -import Box from '../common/Box'; -import Image from '../common/Image'; -import { SyntheticEvent, useContext } from 'react'; -import Space from '../common/Space'; -import Flex from '../common/Flex'; -import SmallTopicPin from '../../assets/smallTopicPin.svg'; -import SmallTopicStar from '../../assets/smallTopicStar.svg'; -import { DEFAULT_TOPIC_IMAGE } from '../../constants'; -import { ModalContext } from '../../context/ModalContext'; - -const FAVORITE_COUNT = 10; - -export interface ModalTopicCardProps { - topicId: number; - topicImage: string; - topicTitle: string; - topicCreator: string; - topicUpdatedAt: string; - topicPinCount: number; - topicClick: any; - topicBookmarkCount: number; -} - -const ModalTopicCard = ({ - topicId, - topicImage, - topicTitle, - topicCreator, - topicUpdatedAt, - topicPinCount, - topicClick, - topicBookmarkCount, -}: ModalTopicCardProps) => { - const { routePage } = useNavigator(); - const { closeModal } = useContext(ModalContext); - const goToSelectedTopic = (topic: any, type: 'newPin' | 'addToTopic') => { - if (type === 'newPin') { - topicClick(topic); - closeModal('newPin'); - } - if (type === 'addToTopic') { - topicClick(topic); - } - }; - - return ( - { - goToSelectedTopic({ topicId, topicTitle }, 'newPin'); - }} - > - - ) => { - e.currentTarget.src = DEFAULT_TOPIC_IMAGE; - }} - /> - - - - - {topicTitle} - - - - - {topicCreator} - - - - - - {topicUpdatedAt.split('T')[0].replaceAll('-', '.')} 업데이트 - - - - - - - - - - {topicPinCount > 999 ? '+999' : topicPinCount}개 - - - - - - - {topicBookmarkCount > 999 ? '+999' : topicBookmarkCount}명 - - - - - - - ); -}; - -const Wrapper = styled.li` - width: 332px; - height: 140px; - cursor: pointer; - border: 1px solid ${({ theme }) => theme.color.gray}; - border-radius: ${({ theme }) => theme.radius.small}; -`; - -const TopicImage = styled(Image)` - border-radius: ${({ theme }) => theme.radius.small}; -`; - -export default ModalTopicCard; diff --git a/frontend/src/components/MyInfo/UpdateMyInfo.tsx b/frontend/src/components/MyInfo/UpdateMyInfo.tsx index 04441b4d..7268ca82 100644 --- a/frontend/src/components/MyInfo/UpdateMyInfo.tsx +++ b/frontend/src/components/MyInfo/UpdateMyInfo.tsx @@ -1,19 +1,16 @@ import { styled } from 'styled-components'; import Flex from '../common/Flex'; -import InfoDefalutImg from '../../assets/InfoDefalutImg.svg'; -import ModifyMyInfoIcon from '../../assets/ModifyMyInfoIcon.svg'; +import ProfileDefaultImage from '../../assets/profile_defaultImage.svg'; import Box from '../common/Box'; -import Text from '../common/Text'; import Space from '../common/Space'; -import { useEffect, useState } from 'react'; -import { MyInfoType } from '../../types/MyInfo'; +import { ProfileProps } from '../../types/Profile'; import Button from '../common/Button'; interface UpdateMyInfoProps { isThereImg: boolean; - myInfoNameAndEmail: MyInfoType; + myInfoNameAndEmail: ProfileProps; setIsModifyMyInfo: React.Dispatch>; - setMyInfoNameAndEmail: React.Dispatch>; + setMyInfoNameAndEmail: React.Dispatch>; } const UpdateMyInfo = ({ @@ -23,12 +20,12 @@ const UpdateMyInfo = ({ setMyInfoNameAndEmail, }: UpdateMyInfoProps) => { const onChangeMyInfoName = (e: React.ChangeEvent) => { - if(e.target.value.length >= 20) return; + if (e.target.value.length >= 20) return; setMyInfoNameAndEmail({ ...myInfoNameAndEmail, name: e.target.value }); }; const onChangeMyInfoEmail = (e: React.ChangeEvent) => { - if(e.target.value.length >= 35) return; + if (e.target.value.length >= 35) return; setMyInfoNameAndEmail({ ...myInfoNameAndEmail, email: e.target.value }); }; @@ -47,7 +44,7 @@ const UpdateMyInfo = ({ {isThereImg ? ( ) : ( - + )} diff --git a/frontend/src/components/MyInfo/index.tsx b/frontend/src/components/MyInfo/index.tsx index 1d2706e0..b2aa0fd8 100644 --- a/frontend/src/components/MyInfo/index.tsx +++ b/frontend/src/components/MyInfo/index.tsx @@ -4,7 +4,7 @@ import Box from '../common/Box'; import Text from '../common/Text'; import Space from '../common/Space'; import { useState } from 'react'; -import { MyInfoType } from '../../types/MyInfo'; +import { ProfileProps } from '../../types/Profile'; import UpdateMyInfo from './UpdateMyInfo'; const user = JSON.parse(localStorage.getItem('user') || '{}'); @@ -12,7 +12,7 @@ const user = JSON.parse(localStorage.getItem('user') || '{}'); const MyInfo = () => { const [isThereImg, setIsThereImg] = useState(true); const [isModifyMyInfo, setIsModifyMyInfo] = useState(false); - const [myInfoNameAndEmail, setMyInfoNameAndEmail] = useState({ + const [myInfoNameAndEmail, setMyInfoNameAndEmail] = useState({ name: user.nickName, email: user.email, }); @@ -39,7 +39,7 @@ const MyInfo = () => { - + {user.nickName} diff --git a/frontend/src/components/MyInfoContainer/MyInfoList/index.tsx b/frontend/src/components/MyInfoContainer/MyInfoList/index.tsx deleted file mode 100644 index 94a05e42..00000000 --- a/frontend/src/components/MyInfoContainer/MyInfoList/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Fragment, useEffect, useState } from 'react'; -import { styled } from 'styled-components'; -import { getApi } from '../../../apis/getApi'; -import PinCard from '../../PinCard'; -import TopicCard from '../../TopicCard'; -import { TopicType } from '../../../types/Topic'; -import useToast from '../../../hooks/useToast'; - -const MyInfoList = () => { - const [myInfoTopics, setMyInfoTopics] = useState([]); - const { showToast } = useToast(); - - const getMyInfoListFromServer = async () => { - try { - const serverMyInfoTopics = await getApi( - 'default', - '/members/my/topics', - ); - - setMyInfoTopics(serverMyInfoTopics); - } catch { - showToast('error', '로그인 후 이용해주세요.'); - } - }; - - useEffect(() => { - getMyInfoListFromServer(); - }, []); - - if (!myInfoTopics) return <>; - - return ( - - {myInfoTopics.map((topic, index) => { - return ( - - - - ); - })} - - ); -}; - -const MyInfoListWrapper = styled.ul` - display: flex; - flex-wrap: wrap; - gap: 20px; -`; - -export default MyInfoList; diff --git a/frontend/src/components/MyInfoContainer/index.tsx b/frontend/src/components/MyInfoContainer/index.tsx deleted file mode 100644 index b2e38cfd..00000000 --- a/frontend/src/components/MyInfoContainer/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { styled } from 'styled-components'; -import Flex from '../common/Flex'; -import Text from '../common/Text'; -import Box from '../common/Box'; -import Space from '../common/Space'; -import { lazy, Suspense } from 'react'; -import TopicCardListSkeleton from '../TopicCardList/TopicCardListSkeleton'; -import Button from '../common/Button'; - -const MyInfoList = lazy(() => import('./MyInfoList')); - -interface MyInfoContainerProps { - containerTitle: string; - containerDescription: string; -} - -const MyInfoContainer = ({ - containerTitle, - containerDescription, -}: MyInfoContainerProps) => ( -
- - - - {containerTitle} - - - - {containerDescription} - - - - - - - }> - - -
-); - -const SeeAllButton = styled(Button)` - cursor: pointer; -`; - -export default MyInfoContainer; diff --git a/frontend/src/components/PinsOfTopic/index.tsx b/frontend/src/components/PinsOfTopic/index.tsx index da2aae33..fc3ee771 100644 --- a/frontend/src/components/PinsOfTopic/index.tsx +++ b/frontend/src/components/PinsOfTopic/index.tsx @@ -1,12 +1,12 @@ import { DEFAULT_TOPIC_IMAGE } from '../../constants'; -import { TopicDetailType } from '../../types/Topic'; +import { TopicDetailProps } from '../../types/Topic'; import PinPreview from '../PinPreview'; import TopicInfo from '../TopicInfo'; interface PinsOfTopicProps { topicId: string; idx: number; - topicDetail: TopicDetailType; + topicDetail: TopicDetailProps; setSelectedPinId: React.Dispatch>; setIsEditPinDetail: React.Dispatch>; setTopicsFromServer: () => void; diff --git a/frontend/src/components/SeeAllCardList/SeeAllCardListSkeleton.tsx b/frontend/src/components/SeeAllCardList/SeeAllCardListSkeleton.tsx deleted file mode 100644 index d78b972e..00000000 --- a/frontend/src/components/SeeAllCardList/SeeAllCardListSkeleton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Flex from '../common/Flex'; -import Space from '../common/Space'; -import TopicCardSkeleton from '../TopicCardSkeleton'; - -const SeeAllCardListSkeleton = () => { - return ( - - - - - - - - - - ); -}; - -export default SeeAllCardListSkeleton; diff --git a/frontend/src/components/SeeAllCardList/index.tsx b/frontend/src/components/SeeAllCardList/index.tsx deleted file mode 100644 index 78c22864..00000000 --- a/frontend/src/components/SeeAllCardList/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Fragment, useEffect, useState } from 'react'; -import { TopicType } from '../../types/Topic'; -import { getApi } from '../../apis/getApi'; -import TopicCard from '../TopicCard'; -import Flex from '../common/Flex'; -import { styled } from 'styled-components'; - -interface SeeAllCardListProps { - url: string; -} - -const SeeAllCardList = ({ url }: SeeAllCardListProps) => { - const [topics, setTopics] = useState([]); - - const getAndSetDataFromServer = async () => { - const topics = await getApi('default', url); - setTopics(topics); - }; - - useEffect(() => { - getAndSetDataFromServer(); - }, []); - - return ( - - {topics && - topics.map((topic) => ( - - - - ))} - - ); -}; - -const Wrapper = styled.ul` - display: flex; - flex-wrap: wrap; - gap: 20px; -`; - -export default SeeAllCardList; diff --git a/frontend/src/components/SeeTogetherCounter/index.tsx b/frontend/src/components/SeeTogetherCounter/index.tsx index ef720be8..4c372916 100644 --- a/frontend/src/components/SeeTogetherCounter/index.tsx +++ b/frontend/src/components/SeeTogetherCounter/index.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect } from 'react'; import { SeeTogetherContext } from '../../context/SeeTogetherContext'; import { keyframes, styled } from 'styled-components'; import { getApi } from '../../apis/getApi'; -import { TopicType } from '../../types/Topic'; +import { TopicCardProps } from '../../types/Topic'; import useToast from '../../hooks/useToast'; const SeeTogetherCounter = () => { @@ -15,7 +15,7 @@ const SeeTogetherCounter = () => { try { if (!userToken) return; - const topics = await getApi('default', '/members/my/atlas'); + const topics = await getApi('/members/my/atlas'); setSeeTogetherTopics(topics); } catch { showToast( diff --git a/frontend/src/components/PinPreviewSkeleton/index.tsx b/frontend/src/components/Skeletons/PinPreviewSkeleton.tsx similarity index 100% rename from frontend/src/components/PinPreviewSkeleton/index.tsx rename to frontend/src/components/Skeletons/PinPreviewSkeleton.tsx diff --git a/frontend/src/components/PinsOfTopic/PinsOfTopicSkeleton.tsx b/frontend/src/components/Skeletons/PinsOfTopicSkeleton.tsx similarity index 79% rename from frontend/src/components/PinsOfTopic/PinsOfTopicSkeleton.tsx rename to frontend/src/components/Skeletons/PinsOfTopicSkeleton.tsx index 0d2c5b19..ffe99d46 100644 --- a/frontend/src/components/PinsOfTopic/PinsOfTopicSkeleton.tsx +++ b/frontend/src/components/Skeletons/PinsOfTopicSkeleton.tsx @@ -1,7 +1,7 @@ import Flex from '../common/Flex'; -import PinPreviewSkeleton from '../PinPreviewSkeleton'; +import PinPreviewSkeleton from './PinPreviewSkeleton'; import Space from '../common/Space'; -import TopicInfoSkeleton from '../TopicInfoSkeleton'; +import TopicInfoSkeleton from './TopicInfoSkeleton'; const PinsOfTopicSkeleton = () => { return ( diff --git a/frontend/src/components/TopicCardSkeleton/index.tsx b/frontend/src/components/Skeletons/TopicCardSkeleton.tsx similarity index 100% rename from frontend/src/components/TopicCardSkeleton/index.tsx rename to frontend/src/components/Skeletons/TopicCardSkeleton.tsx diff --git a/frontend/src/components/TopicInfoSkeleton/index.tsx b/frontend/src/components/Skeletons/TopicInfoSkeleton.tsx similarity index 100% rename from frontend/src/components/TopicInfoSkeleton/index.tsx rename to frontend/src/components/Skeletons/TopicInfoSkeleton.tsx diff --git a/frontend/src/components/TopicCardList/TopicCardListSkeleton.tsx b/frontend/src/components/Skeletons/TopicListSkeleton.tsx similarity index 73% rename from frontend/src/components/TopicCardList/TopicCardListSkeleton.tsx rename to frontend/src/components/Skeletons/TopicListSkeleton.tsx index d274e085..5c30b815 100644 --- a/frontend/src/components/TopicCardList/TopicCardListSkeleton.tsx +++ b/frontend/src/components/Skeletons/TopicListSkeleton.tsx @@ -1,7 +1,7 @@ import { styled } from 'styled-components'; -import TopicCardSkeleton from '../TopicCardSkeleton'; +import TopicCardSkeleton from './TopicCardSkeleton'; -const TopicCardListSkeleton = () => { +const TopicCardContainerSkeleton = () => { return ( @@ -22,4 +22,4 @@ const Wrapper = styled.section` height: 300px; `; -export default TopicCardListSkeleton; +export default TopicCardContainerSkeleton; diff --git a/frontend/src/components/TopicCard/index.tsx b/frontend/src/components/TopicCard/index.tsx index 19038b31..86953c29 100644 --- a/frontend/src/components/TopicCard/index.tsx +++ b/frontend/src/components/TopicCard/index.tsx @@ -3,7 +3,7 @@ import Text from '../common/Text'; import useNavigator from '../../hooks/useNavigator'; import Box from '../common/Box'; import Image from '../common/Image'; -import { SyntheticEvent } from 'react'; +import { SyntheticEvent, useContext } from 'react'; import Space from '../common/Space'; import Flex from '../common/Flex'; import FavoriteSVG from '../../assets/favoriteBtn_filled.svg'; @@ -15,14 +15,23 @@ import SmallTopicStar from '../../assets/smallTopicStar.svg'; import { DEFAULT_TOPIC_IMAGE } from '../../constants'; import AddSeeTogether from '../AddSeeTogether'; import AddFavorite from '../AddFavorite'; -import { TopicType } from '../../types/Topic'; +import { TopicCardProps } from '../../types/Topic'; import useKeyDown from '../../hooks/useKeyDown'; +import { ModalContext } from '../../context/ModalContext'; -interface TopicCardProps extends TopicType { - setTopicsFromServer: () => void; +interface OnClickDesignatedProps { + topicId: number; + topicName: string; +} + +interface TopicCardExtendedProps extends TopicCardProps { + cardType: 'default' | 'modal'; + onClickDesignated?: ({ topicId, topicName }: OnClickDesignatedProps) => void; + getTopicsFromServer?: () => void; } const TopicCard = ({ + cardType, id, image, creator, @@ -32,19 +41,29 @@ const TopicCard = ({ bookmarkCount, isInAtlas, isBookmarked, - setTopicsFromServer, -}: TopicCardProps) => { + onClickDesignated, + getTopicsFromServer, +}: TopicCardExtendedProps) => { const { routePage } = useNavigator(); + const { closeModal } = useContext(ModalContext); const { elementRef, onElementKeyDown } = useKeyDown(); const goToSelectedTopic = () => { routePage(`/topics/${id}`, [id]); }; + const addPinToThisTopic = () => { + if (onClickDesignated) { + onClickDesignated({ topicId: id, topicName: name }); + } + + closeModal('newPin'); + }; + return ( @@ -116,22 +135,24 @@ const TopicCard = ({ - - - {isInAtlas ? : } - - - {isBookmarked ? : } - - + {cardType === 'default' && getTopicsFromServer && ( + + + {isInAtlas ? : } + + + {isBookmarked ? : } + + + )}
diff --git a/frontend/src/components/TopicCardContainer/index.tsx b/frontend/src/components/TopicCardContainer/index.tsx new file mode 100644 index 00000000..a8d0631a --- /dev/null +++ b/frontend/src/components/TopicCardContainer/index.tsx @@ -0,0 +1,122 @@ +import { styled } from 'styled-components'; +import Flex from '../common/Flex'; +import Text from '../common/Text'; +import Box from '../common/Box'; +import Space from '../common/Space'; +import { Fragment, useEffect, useState } from 'react'; +import { TopicCardProps } from '../../types/Topic'; +import useKeyDown from '../../hooks/useKeyDown'; +import TopicCard from '../TopicCard'; +import useGet from '../../apiHooks/useGet'; + +interface TopicCardContainerProps { + url: string; + containerTitle: string; + containerDescription: string; + routeWhenSeeAll: () => void; +} + +const TopicCardContainer = ({ + url, + containerTitle, + containerDescription, + routeWhenSeeAll, +}: TopicCardContainerProps) => { + const [topics, setTopics] = useState(null); + const { elementRef, onElementKeyDown } = useKeyDown(); + const { fetchGet } = useGet(); + + const setTopicsFromServer = async () => { + await fetchGet( + url, + '지도를 가져오는데 실패했습니다. 잠시 후 다시 시도해주세요.', + (response) => { + setTopics(response); + }, + ); + }; + + useEffect(() => { + setTopicsFromServer(); + }, []); + + return ( +
+ + + + {containerTitle} + + + + {containerDescription} + + + + + 전체 보기 + + + + + + + {topics && + topics.map((topic, index) => { + return ( + index < 6 && ( + + + + ) + ); + })} + +
+ ); +}; + +const PointerText = styled(Text)` + cursor: pointer; +`; + +const TopicsWrapper = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 20px; + height: 300px; + overflow: hidden; +`; + +export default TopicCardContainer; diff --git a/frontend/src/components/TopicCardList/index.tsx b/frontend/src/components/TopicCardList/index.tsx index 48fec5d7..8b1df9c2 100644 --- a/frontend/src/components/TopicCardList/index.tsx +++ b/frontend/src/components/TopicCardList/index.tsx @@ -1,53 +1,99 @@ -import { Fragment, useContext, useEffect, useState } from 'react'; -import { getApi } from '../../apis/getApi'; -import { TopicType } from '../../types/Topic'; +import { Fragment, useEffect, useState } from 'react'; +import { styled } from 'styled-components'; import TopicCard from '../TopicCard'; -import { MarkerContext } from '../../context/MarkerContext'; +import { TopicCardProps } from '../../types/Topic'; +import useGet from '../../apiHooks/useGet'; import Flex from '../common/Flex'; +import Space from '../common/Space'; +import Text from '../common/Text'; +import Button from '../common/Button'; -interface TopicCardList { - topics: TopicType[]; - setTopicsFromServer: () => void; +interface TopicCardListProps { + url: string; + errorMessage: string; + commentWhenEmpty: string; + pageCommentWhenEmpty: string; + routePage: () => void; + children?: React.ReactNode; } -const TopicCardList = ({ topics, setTopicsFromServer }: TopicCardList) => { - const { markers, removeMarkers, removeInfowindows } = - useContext(MarkerContext); +const TopicCardList = ({ + url, + errorMessage, + commentWhenEmpty, + pageCommentWhenEmpty, + routePage, + children, +}: TopicCardListProps) => { + const [topics, setTopics] = useState(null); + const { fetchGet } = useGet(); + + const getTopicsFromServer = async () => { + fetchGet(url, errorMessage, (response) => { + setTopics(response); + }); + }; useEffect(() => { - if (markers.length > 0) { - removeMarkers(); - removeInfowindows(); - } + getTopicsFromServer(); }, []); + if (!topics) return <>; + + if (topics.length === 0) { + return ( + + + {children} + + + {commentWhenEmpty} + + + + + + + ); + } + return ( -
    - - {topics && - topics.map((topic, index) => { - return ( - index < 6 && ( - - - - ) - ); - })} - -
+ + {topics.map((topic) => ( + + + + ))} + ); }; +const EmptyWrapper = styled.section` + height: 240px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const Wrapper = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 20px; +`; + export default TopicCardList; diff --git a/frontend/src/components/TopicListContainer/index.tsx b/frontend/src/components/TopicListContainer/index.tsx deleted file mode 100644 index 54f4a4c6..00000000 --- a/frontend/src/components/TopicListContainer/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { styled } from 'styled-components'; -import Flex from '../common/Flex'; -import Text from '../common/Text'; -import Box from '../common/Box'; -import Space from '../common/Space'; -import { lazy, Suspense } from 'react'; -import TopicCardListSkeleton from '../TopicCardList/TopicCardListSkeleton'; -import { TopicType } from '../../types/Topic'; -import useKeyDown from '../../hooks/useKeyDown'; - -const TopicCardList = lazy(() => import('../TopicCardList')); - -interface TopicListContainerProps { - containerTitle: string; - containerDescription: string; - routeWhenSeeAll: () => void; - topics: TopicType[]; - setTopicsFromServer: () => void; -} - -const TopicListContainer = ({ - containerTitle, - containerDescription, - routeWhenSeeAll, - topics, - setTopicsFromServer, -}: TopicListContainerProps) => { - const { elementRef, onElementKeyDown } = useKeyDown(); - - return ( -
- - - - {containerTitle} - - - - {containerDescription} - - - - - 전체 보기 - - - - - - }> - - -
- ); -}; - -const PointerText = styled(Text)` - cursor: pointer; -`; - -export default TopicListContainer; diff --git a/frontend/src/context/SeeTogetherContext.tsx b/frontend/src/context/SeeTogetherContext.tsx index 311fc7cf..81af69af 100644 --- a/frontend/src/context/SeeTogetherContext.tsx +++ b/frontend/src/context/SeeTogetherContext.tsx @@ -5,11 +5,11 @@ import { createContext, useState, } from 'react'; -import { TopicType } from '../types/Topic'; +import { TopicCardProps } from '../types/Topic'; interface SeeTogetherContextProps { - seeTogetherTopics: TopicType[] | null; - setSeeTogetherTopics: Dispatch>; + seeTogetherTopics: TopicCardProps[] | null; + setSeeTogetherTopics: Dispatch>; } interface SeeTogetherProviderProps { @@ -23,7 +23,7 @@ export const SeeTogetherContext = createContext({ const SeeTogetherProvider = ({ children }: SeeTogetherProviderProps) => { const [seeTogetherTopics, setSeeTogetherTopics] = useState< - TopicType[] | null + TopicCardProps[] | null >(null); return ( diff --git a/frontend/src/hooks/useMapClick.ts b/frontend/src/hooks/useMapClick.ts index 4a7f9bc0..c0f67dfd 100644 --- a/frontend/src/hooks/useMapClick.ts +++ b/frontend/src/hooks/useMapClick.ts @@ -7,24 +7,26 @@ export default function useMapClick(map: any) { const { setClickedCoordinate } = useContext(CoordinatesContext); const { showToast } = useToast(); - useEffect(() => { - if (!map) return; - const clickHandler = async (evt: any) => { + const clickHandler = async (evt: any) => { + try { const roadName = await getAddressFromServer( evt.data.lngLat._lat, evt.data.lngLat._lng, ); - if (roadName.id) { - showToast('error', `제공되지 않는 주소 범위입니다.`); - } - setClickedCoordinate({ latitude: evt.data.lngLat._lat, longitude: evt.data.lngLat._lng, address: roadName, }); - }; + } catch (e) { + showToast('error', `제공되지 않는 주소 범위입니다.`); + } + }; + + useEffect(() => { + if (!map) return; + map.on('Click', clickHandler); return () => { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index d5d0e6e6..314ac3d8 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -4,7 +4,7 @@ import { ThemeProvider } from 'styled-components'; import theme from './themes'; import GlobalStyle from './GlobalStyle'; import ErrorBoundary from './components/ErrorBoundary'; -import NotFound from './components/NotFound'; +import NotFound from './pages/NotFound'; const rootElement = document.getElementById('root'); if (!rootElement) throw new Error('Failed to find the root element'); diff --git a/frontend/src/lib/getAddressFromServer.ts b/frontend/src/lib/getAddressFromServer.ts index 42ad45e2..3703a9b2 100644 --- a/frontend/src/lib/getAddressFromServer.ts +++ b/frontend/src/lib/getAddressFromServer.ts @@ -1,19 +1,17 @@ import { getMapApi } from '../apis/getMapApi'; +import { MapAddressProps } from '../types/Map'; -const getAddressFromServer = async (lat: any, lng: any) => { +const getAddressFromServer = async (lat: number, lng: number) => { const version = '1'; const coordType = 'WGS84GEO'; const addressType = 'A10'; const callback = 'result'; - const addressData = await getMapApi( + + const addressData = await getMapApi( `https://apis.openapi.sk.com/tmap/geo/reversegeocoding?version=${version}&lat=${lat}&lon=${lng}&coordType=${coordType}&addressType=${addressType}&callback=${callback}&appKey=P2MX6F1aaf428AbAyahIl9L8GsIlES04aXS9hgxo `, ); - if (addressData.error) { - return addressData.error; - } - const addressResult = addressData.addressInfo.fullAddress.split(','); return addressResult[2]; }; diff --git a/frontend/src/pages/LoginError.tsx b/frontend/src/pages/AskLogin.tsx similarity index 96% rename from frontend/src/pages/LoginError.tsx rename to frontend/src/pages/AskLogin.tsx index e6d11fe9..507a793f 100644 --- a/frontend/src/pages/LoginError.tsx +++ b/frontend/src/pages/AskLogin.tsx @@ -7,7 +7,7 @@ import Text from '../components/common/Text'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { DEFAULT_PROD_URL, FULLSCREEN } from '../constants'; -const LoginError = () => { +const AskLogin = () => { const { width } = useSetLayoutWidth(FULLSCREEN); const loginButtonClick = () => { @@ -53,4 +53,4 @@ const NotFoundButton = styled(Button)` border: 1px solid ${({ theme }) => theme.color.white}; `; -export default LoginError; +export default AskLogin; diff --git a/frontend/src/pages/AskLoginPage.tsx b/frontend/src/pages/AskLoginPage.tsx deleted file mode 100644 index bae9f958..00000000 --- a/frontend/src/pages/AskLoginPage.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { keyframes, styled } from 'styled-components'; -import Image from '../components/common/Image'; -import Text from '../components/common/Text'; -import Space from '../components/common/Space'; -import { DEFAULT_PROD_URL } from '../constants'; - -export default function AskLoginPage() { - const loginButtonClick = () => { - window.location.href = `${DEFAULT_PROD_URL}/oauth/kakao`; - }; - - return ( - - - 로그인해서 쓰면 더 재밌지롱~ - - - - - - - ); -} - -const ErrorContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: 100vh; - text-align: center; -`; - -const pulseAnimation = keyframes` - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.2); - } - 100% { - transform: scale(1); - } -`; - -const RetryButton = styled.button` - display: flex; - justify-content: center; - align-items: center; - width: 200px; - height: 200px; - border: none; - border-radius: 50%; - background-color: #fee500; - font-size: 16px; - font-weight: 700; - color: black; - cursor: pointer; - animation: ${pulseAnimation} 1.5s infinite; -`; diff --git a/frontend/src/pages/Bookmark.tsx b/frontend/src/pages/Bookmark.tsx index ff62111d..d4ba4d01 100644 --- a/frontend/src/pages/Bookmark.tsx +++ b/frontend/src/pages/Bookmark.tsx @@ -6,118 +6,65 @@ import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; -import { Suspense, lazy, useEffect, useState } from 'react'; -import FavoriteNotFilledSVG from '../assets/favoriteBtn_notFilled.svg'; -import TopicCardListSkeleton from '../components/TopicCardList/TopicCardListSkeleton'; -import useToast from '../hooks/useToast'; -import { TopicType } from '../types/Topic'; -import { getApi } from '../apis/getApi'; -import Button from '../components/common/Button'; +import { Suspense, lazy } from 'react'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import useNavigator from '../hooks/useNavigator'; +import FavoriteNotFilledSVG from '../assets/favoriteBtn_notFilled.svg'; -const BookmarksList = lazy(() => import('../components/BookmarksList')); +const TopicCardList = lazy(() => import('../components/TopicCardList')); const Bookmark = () => { - const { width } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('favorite'); - const [bookmarks, setBookmarks] = useState(null); - const { showToast } = useToast(); const { routePage } = useNavigator(); - - const getBookmarksFromServer = async () => { - try { - const serverBookmarks = await getApi( - 'default', - '/members/my/bookmarks', - ); - setBookmarks(serverBookmarks); - } catch { - showToast('error', '로그인 후 이용해주세요.'); - } - }; + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('favorite'); const goToHome = () => { routePage('/'); }; - useEffect(() => { - getBookmarksFromServer(); - }, []); - - if (!bookmarks) return <>; - - if (bookmarks.length === 0) { - return ( - - - - - - 버튼을 눌러 지도를 추가해보세요. - - - - - - - ); - } - return ( - -
- - - - - 즐겨찾기 - - - - 즐겨찾기한 지도들을 한 눈에 보세요. - - - + + + + + + 즐겨찾기 + + + + 즐겨찾기한 지도들을 한 눈에 보세요. + + + - + - }> - - -
-
+ }> + + + + + ); }; -const WrapperWhenEmpty = styled.section<{ width: '372px' | '100vw' }>` - width: ${({ width }) => `calc(${width} - 40px)`}; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; - -const PointerText = styled(Text)` - cursor: pointer; -`; - -const BookMarksWrapper = styled(Box)` +const Wrapper = styled.article` width: 1036px; margin: 0 auto; `; diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx deleted file mode 100644 index 24c7fe9a..00000000 --- a/frontend/src/pages/ErrorPage.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useRouteError } from 'react-router-dom'; -import { keyframes, styled } from 'styled-components'; -import useNavigator from '../hooks/useNavigator'; - -interface Error { - statusText: string; - status: number; -} - -export default function RootErrorPage() { - const { routePage } = useNavigator(); - - const error: Error = useRouteError() as Error; - - return ( - -

Oops!

-

Sorry, an unexpected error has occurred.

- routePage('/')}> - {error.status || error.statusText} - -
- ); -} - -const ErrorContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: 100vh; - text-align: center; -`; - -const pulseAnimation = keyframes` - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.2); - } - 100% { - transform: scale(1); - } -`; - -const RetryButton = styled.button` - display: flex; - justify-content: center; - align-items: center; - width: 150px; - height: 150px; - border: none; - border-radius: 50%; - background-color: #ff3b3b; - font-size: 16px; - font-weight: 700; - color: #ffffff; - cursor: pointer; - animation: ${pulseAnimation} 1.5s infinite; -`; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index d5122253..5fa693c5 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,25 +1,24 @@ import Space from '../components/common/Space'; import Box from '../components/common/Box'; import useNavigator from '../hooks/useNavigator'; -import TopicListContainer from '../components/TopicListContainer'; import { styled } from 'styled-components'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { FULLSCREEN } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -import useToast from '../hooks/useToast'; -import { TopicType } from '../types/Topic'; -import { getApi } from '../apis/getApi'; -import { useEffect, useState } from 'react'; -import Text from '../components/common/Text'; +import { Suspense, lazy, useContext, useEffect } from 'react'; +import { MarkerContext } from '../context/MarkerContext'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; + +const TopicListContainer = lazy( + () => import('../components/TopicCardContainer'), +); const Home = () => { - const [popularTopics, setPopularTopics] = useState(null); - const [nearTopics, setNearTopics] = useState(null); - const [newestTopics, setNewestTopics] = useState(null); const { routePage } = useNavigator(); - const { width: _ } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('home'); - const { showToast } = useToast(); + const { markers, removeMarkers, removeInfowindows } = + useContext(MarkerContext); + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('home'); const goToPopularTopics = () => { routePage('see-all/popularity'); @@ -33,124 +32,57 @@ const Home = () => { routePage('see-all/latest'); }; - const getNearTopicsFromServer = async () => { - try { - const topics = await getApi('default', `/topics`); - setNearTopics(topics); - } catch { - showToast( - 'error', - '로그인 정보가 만료되었습니다. 로그아웃 후 다시 로그인 해주세요.', - ); - } - }; - - const getNewestTopicsFromServer = async () => { - try { - const topics = await getApi('default', '/topics/newest'); - setNewestTopics(topics); - } catch { - showToast( - 'error', - '로그인 정보가 만료되었습니다. 로그아웃 후 다시 로그인 해주세요.', - ); - } - }; - - const getPopularTopicsFromServer = async () => { - try { - const topics = await getApi('default', '/topics/bests'); - setPopularTopics(topics); - } catch { - showToast( - 'error', - '로그인 정보가 만료되었습니다. 로그아웃 후 다시 로그인 해주세요.', - ); - } - }; - - const topicsFetchingFromServer = async () => { - await getPopularTopicsFromServer(); - await getNearTopicsFromServer(); - await getNewestTopicsFromServer(); - }; - useEffect(() => { - topicsFetchingFromServer(); + if (markers.length > 0) { + removeMarkers(); + removeInfowindows(); + } }, []); - if (!(popularTopics && nearTopics && newestTopics)) return <>; - - if ( - popularTopics.length === 0 && - nearTopics.length === 0 && - newestTopics.length === 0 - ) { - return ( - - - 추가하기 버튼을 눌러 토픽을 추가해보세요! - - - - 토픽이 없습니다. - - - ); - } - return ( <> - + }> + + + - + + }> + + + - + + }> + + + ); }; -const EmptyWrapper = styled.section` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 1036px; - height: 100vh; - margin: 0 auto; -`; - const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; `; -const ModalWrapper = styled.div` - width: 300px; - height: 300px; - background-color: white; -`; - export default Home; diff --git a/frontend/src/pages/KaKaoRedirectPage.tsx b/frontend/src/pages/KaKaoRedirectPage.tsx index 9c4fbbcb..03e30e0c 100644 --- a/frontend/src/pages/KaKaoRedirectPage.tsx +++ b/frontend/src/pages/KaKaoRedirectPage.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import { getApi } from '../apis/getApi'; import { keyframes, styled } from 'styled-components'; import useNavigator from '../hooks/useNavigator'; import { DEFAULT_PROD_URL } from '../constants'; +import { getLoginApi } from '../apis/getLoginApi'; // const API_URL = // process.env.NODE_ENV === 'production' @@ -13,7 +13,7 @@ import { DEFAULT_PROD_URL } from '../constants'; export const handleOAuthKakao = async (code: string) => { try { const url = `${DEFAULT_PROD_URL}/oauth/login/kakao?code=${code}`; - const data = await getApi('login', url); + const data = await getLoginApi(url); localStorage.setItem('userToken', data.accessToken); localStorage.setItem('user', JSON.stringify(data.member)); diff --git a/frontend/src/pages/NewPin.tsx b/frontend/src/pages/NewPin.tsx index 73af429e..643123a0 100644 --- a/frontend/src/pages/NewPin.tsx +++ b/frontend/src/pages/NewPin.tsx @@ -6,7 +6,7 @@ import Button from '../components/common/Button'; import { postApi } from '../apis/postApi'; import { FormEvent, useContext, useEffect, useState } from 'react'; import { getApi } from '../apis/getApi'; -import { TopicType } from '../types/Topic'; +import { TopicCardProps } from '../types/Topic'; import useNavigator from '../hooks/useNavigator'; import { NewPinFormProps } from '../types/FormValues'; import useFormValues from '../hooks/useFormValues'; @@ -23,6 +23,7 @@ import { ModalContext } from '../context/ModalContext'; import Modal from '../components/Modal'; import { styled } from 'styled-components'; import ModalMyTopicList from '../components/ModalMyTopicList'; +import { getMapApi } from '../apis/getMapApi'; type NewPinFormValueType = Pick< NewPinFormProps, @@ -116,7 +117,7 @@ const NewPin = () => { if (!topic) { //토픽이 없으면 selectedTopic을 통해 토픽을 생성한다. postTopicId = selectedTopic?.topicId; - postName = selectedTopic?.topicTitle; + postName = selectedTopic?.topicName; } if (postTopicId) routePage(`/topics/${postTopicId}`, [postTopicId]); @@ -144,8 +145,7 @@ const NewPin = () => { const addr = data.roadAddress; // 주소 변수 //data를 통해 받아온 값을 Tmap api를 통해 위도와 경도를 구한다. - const { ConvertAdd } = await getApi( - 'tMap', + const { ConvertAdd } = await getMapApi( `https://apis.openapi.sk.com/tmap/geo/convertAddress?version=1&format=json&callback=result&searchTypCd=NtoO&appKey=P2MX6F1aaf428AbAyahIl9L8GsIlES04aXS9hgxo&coordType=WGS84GEO&reqAdd=${addr}`, ); const lat = ConvertAdd.oldLat; @@ -165,16 +165,13 @@ const NewPin = () => { useEffect(() => { const getTopicId = async () => { if (topicId && topicId.split(',').length === 1) { - const data = await getApi('default', `/topics/${topicId}`); + const data = await getApi(`/topics/${topicId}`); setTopic(data); } if (topicId && topicId.split(',').length > 1) { - const topics = await getApi( - 'default', - `/topics/ids?ids=${topicId}`, - ); + const topics = await getApi(`/topics/ids?ids=${topicId}`); setTopic(topics); } @@ -218,8 +215,8 @@ const NewPin = () => { > {topic?.name ? topic?.name - : selectedTopic?.topicTitle - ? selectedTopic?.topicTitle + : selectedTopic?.topicName + ? selectedTopic?.topicName : '지도를 선택해주세요.'} @@ -325,9 +322,9 @@ const ModalContentsWrapper = styled.div` width: 100%; height: 100%; background-color: white; - - text-align: center; - + display: flex; + flex-direction: column; + align-items: center; overflow: scroll; `; diff --git a/frontend/src/pages/NewTopic.tsx b/frontend/src/pages/NewTopic.tsx index 6793b862..5acb370e 100644 --- a/frontend/src/pages/NewTopic.tsx +++ b/frontend/src/pages/NewTopic.tsx @@ -18,7 +18,7 @@ import Modal from '../components/Modal'; import { styled } from 'styled-components'; import { ModalContext } from '../context/ModalContext'; import { getApi } from '../apis/getApi'; -import { Member } from '../types/Login'; +import { MemberProps } from '../types/Login'; import Checkbox from '../components/common/CheckBox'; import { TagContext } from '../context/TagContext'; @@ -41,11 +41,11 @@ const NewTopic = () => { const { navbarHighlights: _ } = useSetNavbarHighlight('addMapOrPin'); const { setTags } = useContext(TagContext); - const [members, setMembers] = useState([]); + const [members, setMembers] = useState([]); useEffect(() => { const getMemberData = async () => { - const memberData = await getApi('default', `/members`); + const memberData = await getApi(`/members`); setMembers(memberData); }; @@ -57,7 +57,7 @@ const NewTopic = () => { const [checkedMemberIds, setCheckedMemberIds] = useState([]); const handleChecked = (isChecked: boolean, id: number) => - setCheckedMemberIds((prev: Member['id'][]) => + setCheckedMemberIds((prev: MemberProps['id'][]) => isChecked ? [...prev, id] : prev.filter((n: number) => n !== id), ); diff --git a/frontend/src/components/NotFound/index.tsx b/frontend/src/pages/NotFound.tsx similarity index 82% rename from frontend/src/components/NotFound/index.tsx rename to frontend/src/pages/NotFound.tsx index 706c5148..4cca6c17 100644 --- a/frontend/src/components/NotFound/index.tsx +++ b/frontend/src/pages/NotFound.tsx @@ -1,10 +1,10 @@ import { styled } from 'styled-components'; -import NotFoundIcon from '../../assets/NotFoundIcon.svg'; -import useNavigator from '../../hooks/useNavigator'; -import Button from '../common/Button'; -import Flex from '../common/Flex'; -import Space from '../common/Space'; -import Text from '../common/Text'; +import NotFoundIcon from '../assets/NotFoundIcon.svg'; +import useNavigator from '../hooks/useNavigator'; +import Button from '../components/common/Button'; +import Flex from '../components/common/Flex'; +import Space from '../components/common/Space'; +import Text from '../components/common/Text'; const NotFound = () => { const { routePage } = useNavigator(); diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index 11cac986..30ef43a1 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -2,7 +2,7 @@ import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; import Text from '../components/common/Text'; import { useContext, useEffect, useState } from 'react'; -import { PinType } from '../types/Pin'; +import { PinProps } from '../types/Pin'; import { getApi } from '../apis/getApi'; import { useSearchParams } from 'react-router-dom'; import Box from '../components/common/Box'; @@ -32,7 +32,7 @@ const PinDetail = ({ setIsEditPinDetail, }: PinDetailProps) => { const [searchParams, setSearchParams] = useSearchParams(); - const [pin, setPin] = useState(null); + const [pin, setPin] = useState(null); const [selectedTopic, setSelectedTopic] = useState(null); //토픽이 없을 때 사용하는 변수 const { showToast } = useToast(); const { @@ -59,7 +59,7 @@ const PinDetail = ({ useEffect(() => { const getPinData = async () => { - const pinData = await getApi('default', `/pins/${pinId}`); + const pinData = await getApi(`/pins/${pinId}`); setPin(pinData); setFormValues({ name: pinData.name, @@ -238,10 +238,10 @@ const ShareButton = styled(Button)` const ModalContentsWrapper = styled.div` width: 100%; height: 100%; + display: flex; + flex-direction: column; + align-items: center; background-color: white; - - text-align: center; - overflow: scroll; `; diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index c6759446..c2a76b89 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,39 +1,73 @@ -import { useState } from 'react'; import { styled } from 'styled-components'; import Box from '../components/common/Box'; import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; import MyInfo from '../components/MyInfo'; -import MyInfoContainer from '../components/MyInfoContainer'; -import useNavigator from '../hooks/useNavigator'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { FULLSCREEN } from '../constants'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import { Suspense, lazy } from 'react'; +import Text from '../components/common/Text'; +import useNavigator from '../hooks/useNavigator'; + +const TopicCardList = lazy(() => import('../components/TopicCardList')); const Profile = () => { const { routePage } = useNavigator(); - const { width: _ } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('profile'); + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('profile'); - const goToPopularTopics = () => { - routePage('/see-all/popularity'); + const goToNewTopic = () => { + routePage('/new-topic'); }; return ( - + + - - + + + + + 나의 지도 + + + + 내가 만든 지도를 확인해보세요. + + + + + + + }> + + + ); }; -const ProfileWrapper = styled(Box)` +const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; `; diff --git a/frontend/src/pages/SeeAllLatestTopics.tsx b/frontend/src/pages/SeeAllLatestTopics.tsx index 91b41a4e..4cd33932 100644 --- a/frontend/src/pages/SeeAllLatestTopics.tsx +++ b/frontend/src/pages/SeeAllLatestTopics.tsx @@ -6,15 +6,19 @@ import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import Box from '../components/common/Box'; import { Suspense, lazy } from 'react'; -import TopicCardListSkeleton from '../components/TopicCardList/TopicCardListSkeleton'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import useNavigator from '../hooks/useNavigator'; -const SeeAllCardList = lazy(() => import('../components/SeeAllCardList')); - -const url = '/topics/newest'; +const TopicCardList = lazy(() => import('../components/TopicCardList')); const SeeAllLatestTopics = () => { - const { width: _ } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('home'); + const { routePage } = useNavigator(); + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('home'); + + const goToHome = () => { + routePage('/'); + }; return ( @@ -25,8 +29,14 @@ const SeeAllLatestTopics = () => { - }> - + }> + ); diff --git a/frontend/src/pages/SeeAllNearTopics.tsx b/frontend/src/pages/SeeAllNearTopics.tsx index 9ffc0415..510b2f8e 100644 --- a/frontend/src/pages/SeeAllNearTopics.tsx +++ b/frontend/src/pages/SeeAllNearTopics.tsx @@ -5,16 +5,20 @@ import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import Box from '../components/common/Box'; import Space from '../components/common/Space'; import Text from '../components/common/Text'; -import TopicCardListSkeleton from '../components/TopicCardList/TopicCardListSkeleton'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import { Suspense, lazy } from 'react'; +import useNavigator from '../hooks/useNavigator'; -const SeeAllCardList = lazy(() => import('../components/SeeAllCardList')); - -const url = '/topics'; +const TopicCardList = lazy(() => import('../components/TopicCardList')); const SeeAllNearTopics = () => { - const { width: _ } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('home'); + const { routePage } = useNavigator(); + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('home'); + + const goToHome = () => { + routePage('/new-topic'); + }; return ( @@ -25,8 +29,14 @@ const SeeAllNearTopics = () => { - }> - + }> + ); diff --git a/frontend/src/pages/SeeAllPopularTopics.tsx b/frontend/src/pages/SeeAllPopularTopics.tsx index 882f13b4..0f1a717d 100644 --- a/frontend/src/pages/SeeAllPopularTopics.tsx +++ b/frontend/src/pages/SeeAllPopularTopics.tsx @@ -6,15 +6,19 @@ import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { FULLSCREEN } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import { Suspense, lazy } from 'react'; -import TopicCardListSkeleton from '../components/TopicCardList/TopicCardListSkeleton'; +import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import useNavigator from '../hooks/useNavigator'; -const SeeAllCardList = lazy(() => import('../components/SeeAllCardList')); - -const url = '/topics/bests'; +const TopicCardList = lazy(() => import('../components/TopicCardList')); const SeeAllTopics = () => { - const { width: _ } = useSetLayoutWidth(FULLSCREEN); - const { navbarHighlights: __ } = useSetNavbarHighlight('home'); + const { routePage } = useNavigator(); + useSetLayoutWidth(FULLSCREEN); + useSetNavbarHighlight('home'); + + const goToHome = () => { + routePage('/'); + }; return ( @@ -25,8 +29,14 @@ const SeeAllTopics = () => { - }> - + }> + ); diff --git a/frontend/src/pages/SeeAllTopics.tsx b/frontend/src/pages/SeeAllTopics.tsx deleted file mode 100644 index 14674343..00000000 --- a/frontend/src/pages/SeeAllTopics.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useLocation } from 'react-router-dom'; -import Text from '../components/common/Text'; -import { lazy, Suspense } from 'react'; -import SeeAllCardListSkeleton from '../components/SeeAllCardList/SeeAllCardListSkeleton'; - -const SeeAllCardList = lazy(() => import('../components/SeeAllCardList')); - -const SeeAllTopics = () => { - const { state } = useLocation(); - const url = state.split('|')[0]; - const title = state.split('|')[1]; - - return ( - <> - - {title} - - }> - - - - ); -}; - -export default SeeAllTopics; diff --git a/frontend/src/pages/SeeTogetherTopics.tsx b/frontend/src/pages/SeeTogetherTopics.tsx index 38d0d9b1..c949b83c 100644 --- a/frontend/src/pages/SeeTogetherTopics.tsx +++ b/frontend/src/pages/SeeTogetherTopics.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useContext } from 'react'; import { SIDEBAR } from '../constants'; import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; @@ -14,15 +14,15 @@ import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import { deleteApi } from '../apis/deleteApi'; import useToast from '../hooks/useToast'; import { getApi } from '../apis/getApi'; -import { TopicType } from '../types/Topic'; +import { TopicCardProps } from '../types/Topic'; const SeeTogetherTopics = () => { const { routePage } = useNavigator(); const { width } = useSetLayoutWidth(SIDEBAR); - const { navbarHighlights } = useSetNavbarHighlight('seeTogether'); const { seeTogetherTopics, setSeeTogetherTopics } = useContext(SeeTogetherContext); const { showToast } = useToast(); + useSetNavbarHighlight('seeTogether'); const goToHome = () => { routePage('/'); @@ -30,7 +30,7 @@ const SeeTogetherTopics = () => { const setTopicsFromServer = async () => { try { - const topics = await getApi('default', '/members/my/atlas'); + const topics = await getApi('/members/my/atlas'); setSeeTogetherTopics(topics); } catch { @@ -93,6 +93,7 @@ const SeeTogetherTopics = () => { {seeTogetherTopics.map((topic, idx) => (
    { updatedAt={topic.updatedAt} isInAtlas={topic.isInAtlas} isBookmarked={topic.isBookmarked} - setTopicsFromServer={setTopicsFromServer} + getTopicsFromServer={setTopicsFromServer} /> {idx !== seeTogetherTopics.length - 1 ? : <>}
diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index ac0723ad..ec63bb2a 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -9,7 +9,7 @@ import { import { styled } from 'styled-components'; import Space from '../components/common/Space'; import Flex from '../components/common/Flex'; -import { TopicDetailType } from '../types/Topic'; +import { TopicDetailProps } from '../types/Topic'; import { useParams, useSearchParams } from 'react-router-dom'; import theme from '../themes'; import PinDetail from './PinDetail'; @@ -20,16 +20,16 @@ import useNavigator from '../hooks/useNavigator'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { LAYOUT_PADDING, SIDEBAR } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -import PinsOfTopicSkeleton from '../components/PinsOfTopic/PinsOfTopicSkeleton'; +import PinsOfTopicSkeleton from '../components/Skeletons/PinsOfTopicSkeleton'; import { TagContext } from '../context/TagContext'; -import { PinType } from '../types/Pin'; +import { PinProps } from '../types/Pin'; const PinsOfTopic = lazy(() => import('../components/PinsOfTopic')); const SelectedTopic = () => { const { topicId } = useParams(); const [searchParams, _] = useSearchParams(); - const [topicDetails, setTopicDetails] = useState( + const [topicDetails, setTopicDetails] = useState( null, ); const [selectedPinId, setSelectedPinId] = useState(null); @@ -42,10 +42,7 @@ const SelectedTopic = () => { const { navbarHighlights: __ } = useSetNavbarHighlight(''); const getAndSetDataFromServer = async () => { - const data = await getApi( - 'default', - `/topics/ids?ids=${topicId}`, - ); + const data = await getApi(`/topics/ids?ids=${topicId}`); const topicHashmap = new Map([]); @@ -54,8 +51,8 @@ const SelectedTopic = () => { // 각 topic의 pin들의 좌표를 가져옴 const newCoordinates: any = []; - data.forEach((topic: TopicDetailType) => { - topic.pins.forEach((pin: PinType) => { + data.forEach((topic: TopicDetailProps) => { + topic.pins.forEach((pin: PinProps) => { newCoordinates.push({ id: pin.id, topicId: topic.id, @@ -68,13 +65,13 @@ const SelectedTopic = () => { setCoordinates(newCoordinates); - data.forEach((topicDetailFromData: TopicDetailType) => + data.forEach((topicDetailFromData: TopicDetailProps) => topicHashmap.set(`${topicDetailFromData.id}`, topicDetailFromData), ); const topicDetailFromData = topicId ?.split(',') - .map((number) => topicHashmap.get(number)) as TopicDetailType[]; + .map((number) => topicHashmap.get(number)) as TopicDetailProps[]; if (!topicDetailFromData) return; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 21aea0a5..ca5a572b 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,13 +7,13 @@ import SelectedTopic from './pages/SelectedTopic'; import SeeAllPopularTopics from './pages/SeeAllPopularTopics'; import SeeAllNearTopics from './pages/SeeAllNearTopics'; import SeeAllLatestTopics from './pages/SeeAllLatestTopics'; -import KakaoRedirectPage from './pages/KaKaoRedirectPage'; +import KakaoRedirectPage from './pages/KakaoRedirectPage'; import { ReactNode } from 'react'; import AuthLayout from './components/Layout/AuthLayout'; -import NotFound from './components/NotFound'; +import NotFound from './pages/NotFound'; import SeeTogetherTopics from './pages/SeeTogetherTopics'; import Profile from './pages/Profile'; -import LoginError from './pages/LoginError'; +import AskLogin from './pages/AskLogin'; import Bookmark from './pages/Bookmark'; interface routeElement { @@ -85,7 +85,7 @@ const routes: routeElement[] = [ }, { path: '/askLogin', - element: , + element: , withAuth: false, }, { diff --git a/frontend/src/types/Api.ts b/frontend/src/types/Api.ts new file mode 100644 index 00000000..9aea7dea --- /dev/null +++ b/frontend/src/types/Api.ts @@ -0,0 +1 @@ +export type ContentTypeType = 'application/json' | 'x-www-form-urlencoded'; diff --git a/frontend/src/types/Bookmarks.ts b/frontend/src/types/Bookmarks.ts index e0f180d0..d9f75def 100644 --- a/frontend/src/types/Bookmarks.ts +++ b/frontend/src/types/Bookmarks.ts @@ -1,4 +1,4 @@ -export interface BookmarksType { +export interface BookmarksProps { id: number; name: string; image: string; diff --git a/frontend/src/types/Login.ts b/frontend/src/types/Login.ts index a79f2a6c..23d871ab 100644 --- a/frontend/src/types/Login.ts +++ b/frontend/src/types/Login.ts @@ -1,4 +1,4 @@ -export interface Member { +export interface MemberProps { id: number; nickName: string; email: string; @@ -6,7 +6,7 @@ export interface Member { updatedAt: string; } -export interface LoginResponse { +export interface LoginResponseProps { accessToken: string; - member: Member; + member: MemberProps; } diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts new file mode 100644 index 00000000..ba5b370a --- /dev/null +++ b/frontend/src/types/Map.ts @@ -0,0 +1,65 @@ +export interface MapAddressProps { + addressInfo: AddressInfoProps; +} + +export interface AddressInfoProps { + addressType: string; + adminDong: string; + adminDongCode: string; + buildingIndex: string; + buildingName: string; + bunji: string; + city_do: string; + eup_myun: string; + fullAddress: string; + gu_gun: string; + legalDong: string; + legalDongCode: string; + mappingDistance: string; + ri: string; + roadCode: string; + roadName: string; +} + +export interface MapProps { + isMobile: boolean; + mouseClickFlag: boolean; + name: string; + _data: MapDataProps; + _object_: MapObjectProps; + _status: MapStatusProps; +} + +export interface MapDataProps { + mapType: number; + maxBounds: {}; + target: string; + container: {}; + vsmMap: {}; + vsmOptions: {}; + minZoomLimit: number; + maxZoomLimit: number; + options: MapOptionsProps; +} + +export interface MapObjectProps { + eventListeners: {}; + getHandlers: string; + fireEvent: string; +} + +export interface MapStatusProps { + zoom: number; + center: {}; + width: number; + height: number; +} + +export interface MapOptionsProps { + draggable: boolean; + measureControl: boolean; + naviControl: boolean; + pinchZoom: boolean; + scaleBar: boolean; + scrollwheel: boolean; +} diff --git a/frontend/src/types/MyInfo.ts b/frontend/src/types/MyInfo.ts deleted file mode 100644 index 8e3b8619..00000000 --- a/frontend/src/types/MyInfo.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface MyInfoType { - name: string; - email: string; -} - -export interface MyInfoTopicType { - id: number; - name: string; - image: string; - pinCount: number; - bookmarkCount: number; - isBookmarked: boolean; - updatedAt: string; -} - -export interface MyInfoPinType { - id: number; - name: string; - address: string; - description: string; - latitude: number; - longitude: number; -} diff --git a/frontend/src/types/Pin.ts b/frontend/src/types/Pin.ts index 1765fb41..8dda38fb 100644 --- a/frontend/src/types/Pin.ts +++ b/frontend/src/types/Pin.ts @@ -1,6 +1,7 @@ -export interface PinType { +export interface PinProps { id: number; name: string; + creator: string; address: string; description: string; latitude: number; diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts new file mode 100644 index 00000000..16f579c8 --- /dev/null +++ b/frontend/src/types/Profile.ts @@ -0,0 +1,4 @@ +export interface ProfileProps { + name: string; + email: string; +} diff --git a/frontend/src/types/Topic.ts b/frontend/src/types/Topic.ts index 8a9dd25b..55759ed7 100644 --- a/frontend/src/types/Topic.ts +++ b/frontend/src/types/Topic.ts @@ -1,6 +1,6 @@ -import { PinType } from './Pin'; +import { PinProps } from './Pin'; -export interface TopicType { +export interface TopicCardProps { id: number; name: string; image: string; @@ -8,11 +8,11 @@ export interface TopicType { pinCount: number; bookmarkCount: number; updatedAt: string; - isInAtlas: false; - isBookmarked: false; + isInAtlas: boolean; + isBookmarked: boolean; } -export interface TopicDetailType { +export interface TopicDetailProps { id: number; image: string; name: string; @@ -21,14 +21,15 @@ export interface TopicDetailType { pinCount: number; bookmarkCount: number; updatedAt: string; - isInAtlas: false; - isBookmarked: false; - pins: PinType[]; + isInAtlas: boolean; + isBookmarked: boolean; + pins: PinProps[]; } -export interface ModalMyTopicType { +export interface ModalTopicCardProps { id: number; name: string; + creator: string; image: string; pinCount: number; bookmarkCount: number; From 5eee31416ea02ce46fd951bbb2d8b46f299ade43 Mon Sep 17 00:00:00 2001 From: ParkGeunCheol <72205402+GC-Park@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:11:07 +0900 Subject: [PATCH 03/53] =?UTF-8?q?[FE]=20refactor/#373=20msw=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 변경된 명세에 맞게 get fetch 관련 msw 수정 * refactor: 즐겨찾기, 모아보기, 지도 추가하기, 핀 추가하기 msw 적용 --- .github/workflows/fe-pull-request.yml | 2 +- frontend/src/apis/deleteApi.ts | 2 + frontend/src/apis/getApi.ts | 5 + frontend/src/apis/postApi.ts | 2 + frontend/src/apis/putApi.ts | 2 + frontend/src/mocks/db/atlas.js | 3 + frontend/src/mocks/db/bestTopics.js | 26 ++++ frontend/src/mocks/db/bookmarks.js | 15 ++ frontend/src/mocks/db/detailTopic.js | 106 +++++++-------- frontend/src/mocks/db/login.js | 13 ++ frontend/src/mocks/db/myTopics.js | 26 ++++ frontend/src/mocks/db/newestTopics.js | 26 ++++ frontend/src/mocks/db/resLogin.js | 13 ++ frontend/src/mocks/db/topics.js | 24 ++-- frontend/src/mocks/handlers.js | 188 ++++++++++++++++++++++++-- frontend/src/pages/AskLogin.tsx | 22 ++- 16 files changed, 393 insertions(+), 82 deletions(-) create mode 100644 frontend/src/mocks/db/atlas.js create mode 100644 frontend/src/mocks/db/bestTopics.js create mode 100644 frontend/src/mocks/db/bookmarks.js create mode 100644 frontend/src/mocks/db/login.js create mode 100644 frontend/src/mocks/db/myTopics.js create mode 100644 frontend/src/mocks/db/newestTopics.js create mode 100644 frontend/src/mocks/db/resLogin.js diff --git a/.github/workflows/fe-pull-request.yml b/.github/workflows/fe-pull-request.yml index 69d681f0..985cdeda 100644 --- a/.github/workflows/fe-pull-request.yml +++ b/.github/workflows/fe-pull-request.yml @@ -3,7 +3,7 @@ name: Frontend CI For Test Validation on: # pull request open과 reopen 시 실행한다. pull_request: - branches: [main, develop] + branches: [main, develop-FE] paths: frontend/** defaults: diff --git a/frontend/src/apis/deleteApi.ts b/frontend/src/apis/deleteApi.ts index 7707d046..28ded489 100644 --- a/frontend/src/apis/deleteApi.ts +++ b/frontend/src/apis/deleteApi.ts @@ -3,6 +3,8 @@ // ? process.env.REACT_APP_API_DEFAULT_PROD // : process.env.REACT_APP_API_DEFAULT_DEV; +const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : DEFAULT_PROD_URL + import { DEFAULT_PROD_URL } from '../constants'; import { ContentTypeType } from '../types/Api'; diff --git a/frontend/src/apis/getApi.ts b/frontend/src/apis/getApi.ts index 08df249b..60b27a80 100644 --- a/frontend/src/apis/getApi.ts +++ b/frontend/src/apis/getApi.ts @@ -5,6 +5,11 @@ import { DEFAULT_PROD_URL } from '../constants'; +const API_URL = + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000' + : DEFAULT_PROD_URL; + interface Headers { 'content-type': string; [key: string]: string; diff --git a/frontend/src/apis/postApi.ts b/frontend/src/apis/postApi.ts index 44009be9..6850f94b 100644 --- a/frontend/src/apis/postApi.ts +++ b/frontend/src/apis/postApi.ts @@ -6,6 +6,8 @@ import { DEFAULT_PROD_URL } from '../constants'; import { ContentTypeType } from '../types/Api'; +const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : DEFAULT_PROD_URL + interface Headers { 'content-type': string; [key: string]: string; diff --git a/frontend/src/apis/putApi.ts b/frontend/src/apis/putApi.ts index 13d87f61..2a3c1a9c 100644 --- a/frontend/src/apis/putApi.ts +++ b/frontend/src/apis/putApi.ts @@ -6,6 +6,8 @@ import { DEFAULT_PROD_URL } from '../constants'; import { ContentTypeType } from '../types/Api'; +const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : DEFAULT_PROD_URL + interface Headers { 'content-type': string; [key: string]: string; diff --git a/frontend/src/mocks/db/atlas.js b/frontend/src/mocks/db/atlas.js new file mode 100644 index 00000000..05c4972a --- /dev/null +++ b/frontend/src/mocks/db/atlas.js @@ -0,0 +1,3 @@ +const atlas = []; + +export default atlas; diff --git a/frontend/src/mocks/db/bestTopics.js b/frontend/src/mocks/db/bestTopics.js new file mode 100644 index 00000000..6d3dcdd3 --- /dev/null +++ b/frontend/src/mocks/db/bestTopics.js @@ -0,0 +1,26 @@ +const bestTopics = [ + { + id: 1, + name: '준팍의 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 3, + isInAtlas: false, + bookmarkCount: 5, + isBookmarked: true, + updatedAt: '2023-08-17T20:44:58.712930015', + }, + { + id: 2, + name: '준팍의 두번째 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 5, + isInAtlas: false, + bookmarkCount: 3, + isBookmarked: false, + updatedAt: '2023-08-17T20:44:58.712945115', + }, +]; + +export default bestTopics \ No newline at end of file diff --git a/frontend/src/mocks/db/bookmarks.js b/frontend/src/mocks/db/bookmarks.js new file mode 100644 index 00000000..5f4e0eec --- /dev/null +++ b/frontend/src/mocks/db/bookmarks.js @@ -0,0 +1,15 @@ +const bookmarks = [ + { + id: 1, + name: '준팍의 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 5, + isInAtlas: false, + bookmarkCount: 0, + isBookmarked: true, + updatedAt: '2023-08-17T20:44:43.857108576', + }, +]; + +export default bookmarks; diff --git a/frontend/src/mocks/db/detailTopic.js b/frontend/src/mocks/db/detailTopic.js index 1252b5a8..617b0fd1 100644 --- a/frontend/src/mocks/db/detailTopic.js +++ b/frontend/src/mocks/db/detailTopic.js @@ -1,75 +1,65 @@ const detailTopic = [ { - id: '1', - name: '선릉 직장인이 추천하는 맛집', - description: '선릉 직장인이 돌아다니면서 기록한 맛집 리스트예요.', - image: 'image', - pinCount: 3, - updatedAt: '2023-07-12', + id: 1, + name: '준팍의 또 토픽', + description: '준팍이 막 만든 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 2, + isInAtlas: false, + bookmarkCount: 0, + isBookmarked: false, + updatedAt: '2023-08-01T17:23:00.123284785', pins: [ { - id: '1', - name: '잇쇼우', - address: '서울특별시 선릉 테헤란로 127길 16', - description: - '돈까스와 모밀, 우동 등 다양한 일식 메뉴가 있어요. 돈까스가 특히 맛있습니다.', - latitude: 37.512, - longitude: 127.102, + id: 1, + name: '매튜의 산스장', + address: '지번 주소', + description: '매튜가 사랑하는 산스장', + creator: '매튜', + latitude: 36.0, + longitude: 128.0, }, { - id: '2', - name: '오또상스시', - address: '서울특별시 선릉 테헤란로 192-46', - description: - '초밥을 파는 곳입니다. 점심 특선 있고 초밥 질이 괜찮습니다. 가격대도 다른 곳에 비해서 양호한 편이고 적당히 생각날...', - latitude: 37.541, - longitude: 127.0782, + id: 2, + name: '매튜의 안갈집', + address: '지번 주소', + description: '매튜가 두번은 안 갈 집', + creator: '매튜', + latitude: 37.0, + longitude: 127.0, }, ], }, { - id: '2', - name: '산스장 모음', - description: - '초밥을 파는 곳입니다. 점심 특선 있고 초밥 질이 괜찮습니다. 가격대도 다른 곳에 비해서 양호한 편이고 적당히 생각날...', - image: 'image', - pinCount: 5, - updatedAt: '2022-12-25', + id: 2, + name: '준팍의 두번째 토픽', + description: '준팍이 막 만든 두번째 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 2, + isInAtlas: false, + bookmarkCount: 0, + isBookmarked: false, + updatedAt: '2023-08-17T20:45:00.123284785', pins: [ { - id: '1', - name: '잇쇼우', - address: '서울특별시 선릉 테헤란로 127길 16', - description: - '돈까스와 모밀, 우동 등 다양한 일식 메뉴가 있어요. 돈까스가 특히 맛있습니다.', - latitude: 37.5788, - longitude: 126.977, + id: 1, + name: '매튜의 산스장', + address: '지번 주소', + description: '매튜가 사랑하는 산스장', + creator: '매튜', + latitude: 36.0, + longitude: 124.0, }, { - id: '2', - name: '오또상스시', - address: '서울특별시 선릉 테헤란로 192-46', - description: - '초밥을 파는 곳입니다. 점심 특선 있고 초밥 질이 괜찮습니다. 가격대도 다른 곳에 비해서 양호한 편이고 적당히 생각날...', - latitude: 37.5658, - longitude: 126.9753, - }, - { - id: '3', - name: '피양콩할마니', - address: '서울특별시 선릉 테헤란로 127길 16', - description: - '백색깔의 콩비지 맛집입니다~ MSG 안들어가요 고소하니 좋네요~', - latitude: 37.5792, - longitude: 126.9943, - }, - { - id: '4', - name: '용용선생', - address: '서울특별시 선릉 테헤란로 192-46', - description: '마라탕 맛집~ 든든하이 회식하기에도 좋습니다.', - latitude: 37.5722, - longitude: 126.9794, + id: 2, + name: '매튜의 안갈집', + address: '지번 주소', + description: '매튜가 두번은 안 갈 집', + creator: '매튜', + latitude: 37.0, + longitude: 127.0, }, ], }, diff --git a/frontend/src/mocks/db/login.js b/frontend/src/mocks/db/login.js new file mode 100644 index 00000000..b2b17537 --- /dev/null +++ b/frontend/src/mocks/db/login.js @@ -0,0 +1,13 @@ +const resLogin = { + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5MjIzMzcyMDM2ODU0Nzc1ODA3IiwiaWF0IjoxNjkyMjcyNjg3LCJleHAiOjE2OTIyNzYyODd9.FWB1RFvk2uGInqGQDkw0SU4Lghzcggh9TSfuDEZvIUo', + member: { + id: 9223372036854775807, + nickName: '모험가03fcb0d', + email: 'yshert@naver.com', + imageUrl: 'https://map-befine-official.github.io/favicon.png', + updatedAt: '2023-08-17T20:44:47.535382743', + }, +}; + +export default resLogin \ No newline at end of file diff --git a/frontend/src/mocks/db/myTopics.js b/frontend/src/mocks/db/myTopics.js new file mode 100644 index 00000000..0f4d4111 --- /dev/null +++ b/frontend/src/mocks/db/myTopics.js @@ -0,0 +1,26 @@ +const myTopics = [ + { + id: 1, + name: '준팍의 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 5, + isInAtlas: false, + bookmarkCount: 0, + isBookmarked: true, + updatedAt: '2023-08-17T20:44:43.786554794', + }, + { + id: 2, + name: '준팍의 두번째 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 3, + isInAtlas: false, + bookmarkCount: 0, + isBookmarked: false, + updatedAt: '2023-08-17T20:44:43.786578894', + }, +]; + +export default myTopics \ No newline at end of file diff --git a/frontend/src/mocks/db/newestTopics.js b/frontend/src/mocks/db/newestTopics.js new file mode 100644 index 00000000..a50b1a78 --- /dev/null +++ b/frontend/src/mocks/db/newestTopics.js @@ -0,0 +1,26 @@ +const newestTopics = [ + { + id: 1, + name: '준팍의 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 3, + isInAtlas: false, + bookmarkCount: 5, + isBookmarked: true, + updatedAt: '2023-08-17T20:44:58.712930015', + }, + { + id: 2, + name: '준팍의 두번째 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', + pinCount: 5, + isInAtlas: false, + bookmarkCount: 3, + isBookmarked: false, + updatedAt: '2023-08-17T20:44:58.712945115', + }, +]; + +export default newestTopics \ No newline at end of file diff --git a/frontend/src/mocks/db/resLogin.js b/frontend/src/mocks/db/resLogin.js new file mode 100644 index 00000000..b2b17537 --- /dev/null +++ b/frontend/src/mocks/db/resLogin.js @@ -0,0 +1,13 @@ +const resLogin = { + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5MjIzMzcyMDM2ODU0Nzc1ODA3IiwiaWF0IjoxNjkyMjcyNjg3LCJleHAiOjE2OTIyNzYyODd9.FWB1RFvk2uGInqGQDkw0SU4Lghzcggh9TSfuDEZvIUo', + member: { + id: 9223372036854775807, + nickName: '모험가03fcb0d', + email: 'yshert@naver.com', + imageUrl: 'https://map-befine-official.github.io/favicon.png', + updatedAt: '2023-08-17T20:44:47.535382743', + }, +}; + +export default resLogin \ No newline at end of file diff --git a/frontend/src/mocks/db/topics.js b/frontend/src/mocks/db/topics.js index a9c32834..15b89ee9 100644 --- a/frontend/src/mocks/db/topics.js +++ b/frontend/src/mocks/db/topics.js @@ -1,17 +1,25 @@ const topics = [ { - id: '1', - name: '선릉 직장인이 추천하는 맛집', - image: 'image', + id: 1, + name: '준팍의 또 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', pinCount: 3, - updatedAt: '2023-07-12', + isInAtlas: false, + bookmarkCount: 5, + isBookmarked: true, + updatedAt: '2023-08-17T20:44:58.712930015', }, { - id: '2', - name: '산스장 모음', - image: 'image', + id: 2, + name: '준팍의 두번째 토픽', + image: 'https://map-befine-official.github.io/favicon.png', + creator: '준팍', pinCount: 5, - updatedAt: '2022-12-25', + isInAtlas: false, + bookmarkCount: 3, + isBookmarked: false, + updatedAt: '2023-08-17T20:44:58.712945115', }, ]; diff --git a/frontend/src/mocks/handlers.js b/frontend/src/mocks/handlers.js index 4ef03a5f..cd13a5a0 100644 --- a/frontend/src/mocks/handlers.js +++ b/frontend/src/mocks/handlers.js @@ -1,7 +1,13 @@ import { rest } from 'msw'; import topics from './db/topics'; +import newestTopics from './db/newestTopics'; +import bestTopics from './db/bestTopics'; import detailTopic from './db/detailTopic'; import tempPins from './db/tempPins'; +import resLogin from './db/resLogin'; +import bookmarks from './db/bookmarks'; +import myTopics from './db/myTopics'; +import atlas from './db/atlas'; export const handlers = [ // 포스트 목록 @@ -25,11 +31,54 @@ export const handlers = [ ); }), + // 인기 급상승 토픽 목록 + rest.get('/topics/bests', (req, res, ctx) => { + const data = bestTopics; + + if (!data) { + return res(ctx.status(403), ctx.json(data)); + } + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), + ); + }), + + // 최신 토픽 목록 + rest.get('/topics/newest', (req, res, ctx) => { + const data = newestTopics; + + if (!data) { + return res(ctx.status(403), ctx.json(data)); + } + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), + ); + }), + // 토픽 디테일 보기 - rest.get('/topics/:id', (req, res, ctx) => { - const topicId = Number(req.params.id); - const data = detailTopic.filter((pin) => Number(pin.id) === topicId); + rest.get('/topics/ids?ids=:id', (req, res, ctx) => { + let data = []; + if (req.url.searchParams.get('ids').split(',').length > 1) { + const topicId = req.url.searchParams.get('ids').split(','); + topicId.forEach((id) => { + detailTopic.forEach((topic) => { + if (Number(topic.id) === Number(id)) { + data.push(topic); + } + }); + }); + } else { + const topicId = Number(req.url.searchParams.get('ids')); + data = detailTopic.filter((topic) => Number(topic.id) === topicId); + } + console.log(data); if (!data) { return res(ctx.status(403), ctx.json(data)); } @@ -37,22 +86,92 @@ export const handlers = [ return res( ctx.set('Content-Type', 'application/json'), ctx.status(200), - ctx.json(data[0]), + ctx.json(data), + ); + }), + + // login + rest.get('/login', (req, res, ctx) => { + const data = resLogin; + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), + ); + }), + + // bookmarks + rest.get('/members/my/bookmarks', (req, res, ctx) => { + const data = bookmarks; + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), + ); + }), + + // 나의 지도 목록 + rest.get('/members/my/topics', (req, res, ctx) => { + const data = myTopics; + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), + ); + }), + + rest.get('/members/my/atlas', (req, res, ctx) => { + const data = atlas; + + return res( + ctx.set('Content-Type', 'application/json'), + ctx.status(200), + ctx.json(data), ); }), // 토픽 생성 rest.post('/topics/new', async (req, res, ctx) => { - const { name, image, description } = await req.json(); + const { name, image, description, pins, publicity, permissionType } = + await req.json(); topics.push({ id: `${topics.length + 1}`, image, name, - description, - pins: [], + creator: '패트릭', + isInAtlas: false, + isBookmarked: false, + bookmarkCount: 5, + pinCount: 0, + updatedAt: '2023-08-17T20:45:00.123284785', + }); + + newestTopics.push({ + id: `${newestTopics.length + 1}`, + image, + name, + creator: '패트릭', + isInAtlas: false, + isBookmarked: false, + bookmarkCount: 5, pinCount: 0, - updatedAt: '2023-07-19', + updatedAt: '2023-08-17T20:45:00.123284785', + }); + + bestTopics.push({ + id: `${bestTopics.length + 1}`, + image, + name, + creator: '패트릭', + isInAtlas: false, + isBookmarked: false, + bookmarkCount: 5, + pinCount: 0, + updatedAt: '2023-08-17T20:45:00.123284785', }); detailTopic.push({ @@ -60,9 +179,13 @@ export const handlers = [ image, name, description, - pins: [], + creator: '패트릭', + isInAtlas: false, + pins: pins, + isBookmarked: false, + bookmarkCount: 5, pinCount: 0, - updatedAt: '2023-07-19', + updatedAt: '2023-08-17T20:45:00.123284785', }); if (!name) { @@ -77,15 +200,18 @@ export const handlers = [ // 핀 생성 rest.post('/pins', async (req, res, ctx) => { - const { topicId, name, address, description } = await req.json(); + const { topicId, name, address, description, latitude, longitude } = + await req.json(); const newPin = { id: `${detailTopic[topicId - 1].pins.length + 1}`, name, description, address, - latitude: '37', - longitude: '127', + latitude: latitude, + longitude: longitude, + legalDongCode: '', + images: [], }; detailTopic[topicId - 1].pins.push(newPin); @@ -101,6 +227,42 @@ export const handlers = [ ); }), + // 즐겨찾기 추가 + rest.post('/bookmarks/topics?id=:id', async (req, res, ctx) => { + const id = req.url.searchParams.get('id'); + + const bookmarkTopic = []; + topics.forEach((topic) => { + if (topic.id === Number(id)) { + bookmarkTopic.push(topic); + } + }); + + bookmarks.push(bookmarkTopic[0]); + + return res( + ctx.status(201), + ctx.set('Location', `/bookmarks/topics/${id}}`), + ); + }), + + // 모아보기 추가 + rest.post('/atlas/topics?id=:id', async (req, res, ctx) => { + const id = req.url.searchParams.get('id'); + + const atlasTopic = []; + topics.forEach((topic) => { + if (topic.id === Number(id)) { + atlasTopic.push(topic); + } + }); + + atlas.push(atlasTopic[0]); + + return res(ctx.status(201)); + }), + + // pin 변경 rest.put('/pins/:id', async (req, res, ctx) => { const { id } = req.params; const { name, image, description } = await req.json(); diff --git a/frontend/src/pages/AskLogin.tsx b/frontend/src/pages/AskLogin.tsx index 507a793f..b4d10eb7 100644 --- a/frontend/src/pages/AskLogin.tsx +++ b/frontend/src/pages/AskLogin.tsx @@ -6,12 +6,30 @@ import Space from '../components/common/Space'; import Text from '../components/common/Text'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { DEFAULT_PROD_URL, FULLSCREEN } from '../constants'; +import { getApi } from '../apis/getApi'; +import useNavigator from '../hooks/useNavigator'; + +const API_URL = + process.env.NODE_ENV === 'production' + ? process.env.REACT_APP_API_DEFAULT_PROD + : 'http://localhost:3000'; const AskLogin = () => { + const { routePage } = useNavigator(); const { width } = useSetLayoutWidth(FULLSCREEN); - const loginButtonClick = () => { - window.location.href = `${DEFAULT_PROD_URL}/oauth/kakao`; + const loginButtonClick = async () => { + if (API_URL === 'http://localhost:3000') { + const data = await getApi('default', '/login'); + + localStorage.setItem('userToken', data.accessToken); + localStorage.setItem('user', JSON.stringify(data.member)); + + routePage('/'); + return; + } + + window.location.href = `${API_URL}/oauth/kakao`; }; return ( From abc47010ab35ea98d02d56d1c5e9663b169405ef Mon Sep 17 00:00:00 2001 From: ParkGeunCheol <72205402+GC-Park@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:23:30 +0900 Subject: [PATCH 04/53] =?UTF-8?q?hotfix:=20login=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/AskLogin.tsx | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/frontend/src/pages/AskLogin.tsx b/frontend/src/pages/AskLogin.tsx index b4d10eb7..c1c7ef3d 100644 --- a/frontend/src/pages/AskLogin.tsx +++ b/frontend/src/pages/AskLogin.tsx @@ -6,30 +6,12 @@ import Space from '../components/common/Space'; import Text from '../components/common/Text'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { DEFAULT_PROD_URL, FULLSCREEN } from '../constants'; -import { getApi } from '../apis/getApi'; -import useNavigator from '../hooks/useNavigator'; - -const API_URL = - process.env.NODE_ENV === 'production' - ? process.env.REACT_APP_API_DEFAULT_PROD - : 'http://localhost:3000'; const AskLogin = () => { - const { routePage } = useNavigator(); const { width } = useSetLayoutWidth(FULLSCREEN); const loginButtonClick = async () => { - if (API_URL === 'http://localhost:3000') { - const data = await getApi('default', '/login'); - - localStorage.setItem('userToken', data.accessToken); - localStorage.setItem('user', JSON.stringify(data.member)); - - routePage('/'); - return; - } - - window.location.href = `${API_URL}/oauth/kakao`; + window.location.href = `${DEFAULT_PROD_URL}/oauth/kakao`; }; return ( From 12613e1ed63b0125779a23cac45935be4c3bfd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:23:55 +0900 Subject: [PATCH 05/53] =?UTF-8?q?[FE]=20Feature/#382=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EB=8C=80=EC=9D=91=20(#387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Home 페이지 모바일 및 태블릿 사이즈 반응형 대응 * feat: 네비게이션 바 반응형 대응 * refactor: 반응형 로직 재사용 및 방식 변경 * feat: 전체보기 페이지 반응형 적용 * refactor: 반응형 재사용 함수 네이밍 수정 * feat: 즐겨찾기 페이지 반응형 대응 * feat: 마이페이지 반응형 대응 * fix: 뒤로가기를 통해 핀을 선택하지 않았던 때로 돌아가도 pinDetail이 남아있는 오류 수정 * refactor: 불필요한 console.log 제거 및 as section 지정 * feat: 토픽 조회 및 지도 반응형 대응 * refactor: interface 타입 오류 및 이미지 radius 수정 * refactor: 토픽 단일 조회 및 상세조회 반응형 대응 * refactor: 토픽 단일 조회 및 상세페이지 태블릿 반응형 대응 방식 변경 * refactor: 토스트 알림 반응형 대응 * refactor: 네비게이션 반응형 오류 및 height 고정 문제 수정 추가하기 모달의 경우 단일 토픽 조회 페이지 등에서 미디어 쿼리보다 layoutWidth가 우선순위가 더 높아 어색하게 배치되는 문제를 해결한다. * refactor: 핀추가하기 페이지 반응형 대응 핀 추가하기 페이지는 지도를 하단에 배치하여 가상키보드가 올라왔을 때를 대응한다. * refactor: 누락된 커밋 추가하기 모달 중앙정렬 추가 * fix: 스타일드 컴포넌트 trasient DOM 에러 해결 * feat: 모달 컴포넌트 반응형 대응 * fix: PC 사이즈에서 position fixed 되는 오류 수정 * feat: 뽑아오기 컴포넌트 반응형 대응 * refactor: 토픽 단일 조회 및 뽑아오기 반응형 대응 일부 수정 * design: 내 지도에 추가하기 모달 가로 사이즈 조정 * refactor: 네이밍 변경 (mediaWidth -> layoutWidth) * feat: 핀 수정 페이지 반응형 대응 및 에러메세지 기능 수정 빈 값을 입력해도 에러메세지가 뜨지 않는 오류를 수정한다. * design: 모바일 사이즈에서 input 태그 간 간격이 좁은 오류 수정 * refactor: 태블릿-모바일 사이즈에서 핀 수정 페이지 반응형 수정 * refactor: merge 브랜치 변경 (develop->develop-FE) * fix: cypress 테스트 자동화 구문 수정 * refactor: jobs 네이밍 수정 * design: navbar 경계선 그림자 추가 * fix: 카카오 대소문자 수정으로 인한 파일 트래킹이 안되는 오류 수정 * test: 반응형 ui 변경으로 인한 cypress 테스트 로직 변경 * chore: github actions 환경에서만 테스트가 실패하는 오류 수정 * test: github actions 환경에서 wait 관련 테스트만 실패하여 wait 타임을 확장 * test: github actions 환경에서만 통과하지 못하는 wait 함수 값 수정 * refactor: 불필요한 로직 제거 * refactor: 불필요한 어트리뷰트 제거 * chore: api 요청 개발서버로 변경 --- .github/workflows/fe-merge-dev.yml | 2 +- .github/workflows/fe-pull-request.yml | 8 +- frontend/cypress.config.ts | 1 + frontend/cypress/e2e/mapbefine.cy.ts | 8 +- .../src/components/InputContainer/index.tsx | 2 +- frontend/src/components/Layout/Navbar.tsx | 60 +++++--- frontend/src/components/Layout/index.tsx | 130 +++++++++------- frontend/src/components/Map/index.tsx | 4 + frontend/src/components/Modal/index.tsx | 42 +++--- .../ModalMyTopicList/addToMyTopicList.tsx | 48 +++--- .../src/components/ModalMyTopicList/index.tsx | 6 + frontend/src/components/MyInfo/index.tsx | 2 +- frontend/src/components/PinsOfTopic/index.tsx | 7 +- frontend/src/components/PullPin/index.tsx | 38 +++-- frontend/src/components/Toast/index.tsx | 11 +- .../components/TopicCardContainer/index.tsx | 3 +- .../src/components/TopicCardList/index.tsx | 1 - frontend/src/components/TopicInfo/index.tsx | 12 +- frontend/src/constants/index.ts | 2 +- frontend/src/constants/responsive.ts | 13 ++ frontend/src/pages/Bookmark.tsx | 5 +- frontend/src/pages/Home.tsx | 70 ++++----- ...aKaoRedirectPage.tsx => KakaoRedirect.tsx} | 4 +- frontend/src/pages/NewPin.tsx | 105 +++++++------ frontend/src/pages/PinDetail.tsx | 98 ++++++++---- frontend/src/pages/Profile.tsx | 3 + frontend/src/pages/RootPage.tsx | 9 +- frontend/src/pages/SeeAllLatestTopics.tsx | 3 + frontend/src/pages/SeeAllNearTopics.tsx | 3 + frontend/src/pages/SeeAllPopularTopics.tsx | 3 + frontend/src/pages/SelectedTopic.tsx | 141 ++++++++++-------- frontend/src/pages/UpdatedPinDetail.tsx | 63 ++++---- frontend/src/router.tsx | 4 +- 33 files changed, 555 insertions(+), 356 deletions(-) create mode 100644 frontend/src/constants/responsive.ts rename frontend/src/pages/{KaKaoRedirectPage.tsx => KakaoRedirect.tsx} (96%) diff --git a/.github/workflows/fe-merge-dev.yml b/.github/workflows/fe-merge-dev.yml index 33e8ad8e..a9c58c62 100644 --- a/.github/workflows/fe-merge-dev.yml +++ b/.github/workflows/fe-merge-dev.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [develop] + branches: [develop-FE] types: [closed] paths: frontend/** diff --git a/.github/workflows/fe-pull-request.yml b/.github/workflows/fe-pull-request.yml index 985cdeda..909a0170 100644 --- a/.github/workflows/fe-pull-request.yml +++ b/.github/workflows/fe-pull-request.yml @@ -11,7 +11,7 @@ defaults: working-directory: ./frontend jobs: - jest: + jest_cypress: runs-on: ubuntu-22.04 steps: @@ -29,10 +29,8 @@ jobs: - name: Run Jest test run: npm run test - - name: Start npm - run: npm run dev - - name: Run Cypress uses: cypress-io/github-action@v5 with: - start: npm run cypress + working-directory: frontend + start: npm run dev diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 5bed49b8..35938082 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -3,5 +3,6 @@ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', + defaultCommandTimeout: 10000, }, }); diff --git a/frontend/cypress/e2e/mapbefine.cy.ts b/frontend/cypress/e2e/mapbefine.cy.ts index f24052ad..f9a84f34 100644 --- a/frontend/cypress/e2e/mapbefine.cy.ts +++ b/frontend/cypress/e2e/mapbefine.cy.ts @@ -69,12 +69,14 @@ describe('토픽 상세 페이지', () => { if (index === 0) $el.click(); }); - cy.wait(1000); + cy.wait(3000); cy.get('span').each(($el, index) => { if (index === 6) $el.click(); }); + cy.get('[data-cy="pin-detail"]').scrollTo('bottom'); + cy.contains('내 지도에 저장하기').should('be.visible'); }); @@ -85,12 +87,14 @@ describe('토픽 상세 페이지', () => { if (index === 0) $el.click(); }); - cy.wait(1000); + cy.wait(2000); cy.get('span').each(($el, index) => { if (index === 6) $el.click(); }); + cy.get('[data-cy="pin-detail"]').scrollTo('bottom'); + cy.contains('내 지도에 저장하기').click(); cy.contains('로그인 후 사용해주세요.').should('be.visible'); diff --git a/frontend/src/components/InputContainer/index.tsx b/frontend/src/components/InputContainer/index.tsx index 74f53c73..7cc76434 100644 --- a/frontend/src/components/InputContainer/index.tsx +++ b/frontend/src/components/InputContainer/index.tsx @@ -111,7 +111,7 @@ const InputContainer = ({ }; const ErrorText = styled.span` display: block; - height: 20px; + min-height: 20px; font-size: 14px; color: #ff4040; `; diff --git a/frontend/src/components/Layout/Navbar.tsx b/frontend/src/components/Layout/Navbar.tsx index 10de9879..434d057f 100644 --- a/frontend/src/components/Layout/Navbar.tsx +++ b/frontend/src/components/Layout/Navbar.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; import useNavigator from '../../hooks/useNavigator'; import Flex from '../common/Flex'; import Button from '../common/Button'; @@ -73,8 +73,12 @@ const Navbar = ({ $layoutWidth }: NavBarProps) => { }; return ( - + { - - { - - { - - { - - { ); }; -const Wrapper = styled.nav<{ $layoutWidth: '100vw' | '372px' }>` +const Wrapper = styled.nav<{ + $isAddPage: boolean; + $layoutWidth: '100vw' | '372px'; +}>` width: 100%; - height: 64px; + min-height: 56px; display: flex; justify-content: ${({ $layoutWidth }) => $layoutWidth === '100vw' ? 'center' : 'space-around'}; align-items: center; + background-color: ${({ theme }) => theme.color.white}; + z-index: 2; + box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.3); + + @media (max-width: 1076px) { + justify-content: space-around; + + ${({ $isAddPage }) => + $isAddPage && + css` + position: fixed; + bottom: 0; + `} + } `; -const IconWrapper = styled.div` +const IconWrapper = styled.div<{ $layoutWidth: '100vw' | '372px' }>` position: relative; display: flex; flex-direction: column; align-items: center; width: 52px; cursor: pointer; -`; + margin-right: ${({ $layoutWidth }) => + $layoutWidth === '100vw' ? '48px' : '0'}; + + &:last-of-type { + margin-right: 0; + } -const IconSpace = styled(Space)<{ $layoutWidth: '100vw' | '372px' }>` - display: ${({ $layoutWidth }) => - $layoutWidth === '100vw' ? 'block' : 'none'}; + @media (max-width: 1076px) { + margin-right: 0; + } `; const RouteButton = styled(Button)` box-shadow: 2px 4px 4px rgba(0, 0, 0, 0.5); `; -const ModalWrapper = styled(Flex)` - width: 100%; - height: 100%; -`; export default Navbar; diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index 666ca050..f59d4f10 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -6,15 +6,14 @@ import CoordinatesProvider from '../../context/CoordinatesContext'; import MarkerProvider from '../../context/MarkerContext'; import ToastProvider from '../../context/ToastContext'; import Toast from '../Toast'; -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; import { LayoutWidthContext } from '../../context/LayoutWidthContext'; import SeeTogetherProvider from '../../context/SeeTogetherContext'; import Space from '../common/Space'; import Navbar from './Navbar'; import ModalProvider from '../../context/ModalContext'; -import NavbarHighlightsProvider from '../../context/NavbarHighlightsContext'; +import { NavbarHighlightsContext } from '../../context/NavbarHighlightsContext'; import TagProvider from '../../context/TagContext'; -import InfoDefalutImg from '../../assets/InfoDefalutImg.svg'; import Box from '../common/Box'; type LayoutProps = { @@ -32,11 +31,7 @@ const Layout = ({ children }: LayoutProps) => { const { Tmapv3 } = window; const mapContainer = useRef(null); const { width } = useContext(LayoutWidthContext); - const isLogined = localStorage.getItem('userToken'); - - const loginButtonClick = () => { - window.location.href = 'https://mapbefine.kro.kr/api/oauth/kakao'; - }; + const { navbarHighlights } = useContext(NavbarHighlightsContext); const [map, setMap] = useState(null); @@ -54,59 +49,92 @@ const Layout = ({ children }: LayoutProps) => { return ( - - - - - - - + + + + + + + + + + + + - - - - - - - - {children} - - - - - - - - - - - + {children} + + + + + + + + + + ); }; -const LayoutFlex = styled(Flex)` - transition: all ease 0.3s; +const LogoWrapper = styled.section<{ + $layoutWidth: '372px' | '100vw'; +}>` + width: 372px; + display: flex; + padding: 12px 20px 0 20px; + + @media (max-width: 1076px) { + ${({ $layoutWidth }) => + $layoutWidth === '372px' && + css` + width: 100vw; + background-color: white; + position: fixed; + top: 0; + z-index: 1; + `}; + } +`; + +const MediaWrapper = styled.section<{ + $isAddPage: boolean; + $layoutWidth: '372px' | '100vw'; +}>` + display: flex; + width: 100vw; + overflow: hidden; + + @media (max-width: 1076px) { + flex-direction: ${({ $isAddPage, $layoutWidth }) => { + if ($isAddPage) return 'column'; + if ($layoutWidth === '372px') return 'column-reverse'; + }}; + } `; -const MyInfoImg = styled.img` - width: 40px; - height: 40px; +const LayoutFlex = styled(Flex)<{ $layoutWidth: '372px' | '100vw' }>` + transition: all ease 0.3s; - border-radius: 50%; + @media (max-width: 1076px) { + height: ${({ $layoutWidth }) => $layoutWidth === '372px' && '50vh'}; + transition: none; + } `; export default Layout; diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx index 157a46b6..2305fd36 100644 --- a/frontend/src/components/Map/index.tsx +++ b/frontend/src/components/Map/index.tsx @@ -40,6 +40,10 @@ const MapFlex = styled(Flex)` $minWidth === '100vw' ? '0' : 'calc(100vw - 400px)'}; } } + + @media (max-width: 1076px) { + max-height: 50vh; + } `; export default forwardRef(Map); diff --git a/frontend/src/components/Modal/index.tsx b/frontend/src/components/Modal/index.tsx index ee2d8390..d642c6f3 100644 --- a/frontend/src/components/Modal/index.tsx +++ b/frontend/src/components/Modal/index.tsx @@ -75,23 +75,6 @@ const Modal = ({ root, ); }; -const Wrapper = styled.div` - width: ${({ width }) => width || '400px'}; - height: ${({ height }) => height || '400px'}; - ${({ position }) => getModalPosition(position)}; - top: ${({ top }) => top && top}; - left: ${({ left }) => left && left}; - z-index: 2; -`; - -const WrapperDimmed = styled.div<{ $dimmedColor: string }>` - width: 100%; - height: 100%; - position: fixed; - top: 0; - background-color: ${({ $dimmedColor }) => $dimmedColor}; - z-index: 2; -`; const translateModalAnimation = keyframes` from { @@ -113,7 +96,7 @@ const openModalAnimation = keyframes` } `; -const getModalPosition = (position: 'center' | 'bottom' | 'absolute') => { +const getModalPosition = (position: 'center' | 'bottom') => { switch (position) { case 'center': return css` @@ -139,4 +122,27 @@ const getModalPosition = (position: 'center' | 'bottom' | 'absolute') => { } }; +const Wrapper = styled.div` + width: ${({ width }) => width || '400px'}; + height: ${({ height }) => height || '400px'}; + ${({ position }) => getModalPosition(position)}; + top: ${({ top }) => top && top}; + left: ${({ left }) => left && left}; + z-index: 2; + + @media (max-width: 744px) { + ${getModalPosition('bottom')}; + width: 100%; + } +`; + +const WrapperDimmed = styled.div<{ $dimmedColor: string }>` + width: 100%; + height: 100%; + position: fixed; + top: 0; + background-color: ${({ $dimmedColor }) => $dimmedColor}; + z-index: 2; +`; + export default Modal; diff --git a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx index d3ed6c4f..08dcafbd 100644 --- a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx +++ b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx @@ -6,6 +6,7 @@ import { ModalContext } from '../../context/ModalContext'; import useToast from '../../hooks/useToast'; import useGet from '../../apiHooks/useGet'; import usePost from '../../apiHooks/usePost'; +import Space from '../common/Space'; interface OnClickDesignatedProps { topicId: number; @@ -61,25 +62,28 @@ const AddToMyTopicList = ({ pin }: any) => { if (!myTopics) return <>; return ( - - {myTopics.map((topic) => ( - - - - ))} - + <> + + {myTopics.map((topic) => ( + + + + ))} + + + ); }; @@ -89,6 +93,12 @@ const ModalMyTopicListWrapper = styled.ul` display: flex; flex-wrap: wrap; gap: 20px; + + @media (max-width: 744px) { + width: 100%; + justify-content: center; + margin-bottom: 48px; + } `; export default AddToMyTopicList; diff --git a/frontend/src/components/ModalMyTopicList/index.tsx b/frontend/src/components/ModalMyTopicList/index.tsx index 79981091..1d78e36c 100644 --- a/frontend/src/components/ModalMyTopicList/index.tsx +++ b/frontend/src/components/ModalMyTopicList/index.tsx @@ -74,6 +74,12 @@ const ModalMyTopicListWrapper = styled.ul` display: flex; flex-wrap: wrap; gap: 20px; + + @media (max-width: 744px) { + width: 100%; + justify-content: center; + margin-bottom: 48px; + } `; export default ModalMyTopicList; diff --git a/frontend/src/components/MyInfo/index.tsx b/frontend/src/components/MyInfo/index.tsx index b2aa0fd8..87a0f23b 100644 --- a/frontend/src/components/MyInfo/index.tsx +++ b/frontend/src/components/MyInfo/index.tsx @@ -37,7 +37,7 @@ const MyInfo = () => { $alignItems="center" > - + {user.nickName} diff --git a/frontend/src/components/PinsOfTopic/index.tsx b/frontend/src/components/PinsOfTopic/index.tsx index fc3ee771..7a9bba83 100644 --- a/frontend/src/components/PinsOfTopic/index.tsx +++ b/frontend/src/components/PinsOfTopic/index.tsx @@ -1,3 +1,4 @@ +import { styled } from 'styled-components'; import { DEFAULT_TOPIC_IMAGE } from '../../constants'; import { TopicDetailProps } from '../../types/Topic'; import PinPreview from '../PinPreview'; @@ -21,7 +22,7 @@ const PinsOfTopic = ({ setTopicsFromServer, }: PinsOfTopicProps) => { return ( -
    + ))} -
+
); }; +const Wrapper = styled.ul``; + export default PinsOfTopic; diff --git a/frontend/src/components/PullPin/index.tsx b/frontend/src/components/PullPin/index.tsx index 58f2e418..7264d05b 100644 --- a/frontend/src/components/PullPin/index.tsx +++ b/frontend/src/components/PullPin/index.tsx @@ -21,20 +21,11 @@ const PullPin = ({ if (tags.length === 0) return <>; return ( - + @@ -96,8 +87,29 @@ const PullPin = ({ ); }; -const Wrapper = styled(Flex)` - border-bottom: 2px solid ${({ theme }) => theme.color.black}; +const Wrapper = styled.section` + width: 332px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: white; + position: fixed; + border-radius: ${({ theme }) => theme.radius.small}; + z-index: 1; + border-bottom: 4px solid ${({ theme }) => theme.color.black}; + + @media (max-width: 1076px) { + width: calc(50vw - 40px); + } + + @media (max-width: 744px) { + width: calc(100vw - 40px); + } + + @media (max-width: 372px) { + width: 332px; + } `; export default PullPin; diff --git a/frontend/src/components/Toast/index.tsx b/frontend/src/components/Toast/index.tsx index fa286ac0..6d2ac743 100644 --- a/frontend/src/components/Toast/index.tsx +++ b/frontend/src/components/Toast/index.tsx @@ -14,7 +14,12 @@ const Toast = () => { return ReactDOM.createPortal( toast.show && ( - + {toast.message} ), @@ -62,6 +67,10 @@ const Wrapper = styled(Flex)<{ type: string }>` color: ${({ theme }) => theme.color.white}; z-index: 2; + + @media (max-width: 588px) { + width: 80%; + } `; export default Toast; diff --git a/frontend/src/components/TopicCardContainer/index.tsx b/frontend/src/components/TopicCardContainer/index.tsx index a8d0631a..9041ed37 100644 --- a/frontend/src/components/TopicCardContainer/index.tsx +++ b/frontend/src/components/TopicCardContainer/index.tsx @@ -113,10 +113,9 @@ const PointerText = styled(Text)` const TopicsWrapper = styled.ul` display: flex; + justify-content: center; flex-wrap: wrap; gap: 20px; - height: 300px; - overflow: hidden; `; export default TopicCardContainer; diff --git a/frontend/src/components/TopicCardList/index.tsx b/frontend/src/components/TopicCardList/index.tsx index 8b1df9c2..74c15110 100644 --- a/frontend/src/components/TopicCardList/index.tsx +++ b/frontend/src/components/TopicCardList/index.tsx @@ -86,7 +86,6 @@ const EmptyWrapper = styled.section` height: 240px; display: flex; flex-direction: column; - justify-content: center; align-items: center; `; diff --git a/frontend/src/components/TopicInfo/index.tsx b/frontend/src/components/TopicInfo/index.tsx index 40192c0d..1b5f7f18 100644 --- a/frontend/src/components/TopicInfo/index.tsx +++ b/frontend/src/components/TopicInfo/index.tsx @@ -73,9 +73,9 @@ const TopicInfo = ({ role="button" data-cy="topic-info" > - 토픽 이미지 {isInAtlas ? ( @@ -139,7 +139,7 @@ const TopicInfo = ({ {isBookmarked ? : } @@ -158,4 +158,8 @@ const ButtonsWrapper = styled.div` align-items: center; `; +const TopicImage = styled(Image)` + border-radius: ${({ theme }) => theme.radius.medium}; +`; + export default TopicInfo; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 9b12e60b..b55fff9d 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -7,4 +7,4 @@ export const LAYOUT_PADDING = '40px'; export const DEFAULT_TOPIC_IMAGE = 'https://velog.velcdn.com/images/semnil5202/post/37f3bcb9-0b07-4100-85f6-f1d5ad037c14/image.svg'; -export const DEFAULT_PROD_URL = 'https://mapbefine.com/api'; +export const DEFAULT_PROD_URL = 'https://mapbefine.kro.kr/api'; diff --git a/frontend/src/constants/responsive.ts b/frontend/src/constants/responsive.ts new file mode 100644 index 00000000..2e41cf3d --- /dev/null +++ b/frontend/src/constants/responsive.ts @@ -0,0 +1,13 @@ +import { css } from 'styled-components'; + +export const setFullScreenResponsive = () => { + return css` + @media (max-width: 1076px) { + width: 684px; + } + + @media (max-width: 724px) { + width: 332px; + } + `; +}; diff --git a/frontend/src/pages/Bookmark.tsx b/frontend/src/pages/Bookmark.tsx index d4ba4d01..f84d6a42 100644 --- a/frontend/src/pages/Bookmark.tsx +++ b/frontend/src/pages/Bookmark.tsx @@ -10,6 +10,7 @@ import { Suspense, lazy } from 'react'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import useNavigator from '../hooks/useNavigator'; import FavoriteNotFilledSVG from '../assets/favoriteBtn_notFilled.svg'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicCardList = lazy(() => import('../components/TopicCardList')); @@ -53,7 +54,7 @@ const Bookmark = () => { @@ -67,6 +68,8 @@ const Bookmark = () => { const Wrapper = styled.article` width: 1036px; margin: 0 auto; + + ${setFullScreenResponsive()} `; export default Bookmark; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 5fa693c5..748ca403 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,13 +1,14 @@ import Space from '../components/common/Space'; import Box from '../components/common/Box'; import useNavigator from '../hooks/useNavigator'; -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { FULLSCREEN } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import { Suspense, lazy, useContext, useEffect } from 'react'; import { MarkerContext } from '../context/MarkerContext'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicListContainer = lazy( () => import('../components/TopicCardContainer'), @@ -40,49 +41,50 @@ const Home = () => { }, []); return ( - <> - - - }> - - + + + }> + + - + - }> - - + }> + + - + - }> - - + }> + + - - - + + ); }; -const Wrapper = styled(Box)` +const Wrapper = styled.article` width: 1036px; margin: 0 auto; + position: relative; + + ${setFullScreenResponsive()} `; export default Home; diff --git a/frontend/src/pages/KaKaoRedirectPage.tsx b/frontend/src/pages/KakaoRedirect.tsx similarity index 96% rename from frontend/src/pages/KaKaoRedirectPage.tsx rename to frontend/src/pages/KakaoRedirect.tsx index 03e30e0c..f645214c 100644 --- a/frontend/src/pages/KaKaoRedirectPage.tsx +++ b/frontend/src/pages/KakaoRedirect.tsx @@ -23,7 +23,7 @@ export const handleOAuthKakao = async (code: string) => { } }; -const KakaoRedirectPage = () => { +const KakaoRedirect = () => { const { routePage } = useNavigator(); const routerLocation = useLocation(); @@ -48,7 +48,7 @@ const KakaoRedirectPage = () => { ); }; -export default KakaoRedirectPage; +export default KakaoRedirect; const KakaoRedirectPageWrapper = styled.div` display: flex; diff --git a/frontend/src/pages/NewPin.tsx b/frontend/src/pages/NewPin.tsx index 643123a0..dc486642 100644 --- a/frontend/src/pages/NewPin.tsx +++ b/frontend/src/pages/NewPin.tsx @@ -184,7 +184,7 @@ const NewPin = () => { <>
- @@ -194,32 +194,30 @@ const NewPin = () => { -
- - - 지도 선택 - - - - * - - + + + 지도 선택 + - -
+ + * + +
+ + @@ -239,27 +237,25 @@ const NewPin = () => { -
- - - 장소 위치 - - - - * - - + + + 장소 위치 + - -
+ + * + + + + @@ -292,13 +288,13 @@ const NewPin = () => { 추가하기 - + @@ -318,6 +314,19 @@ const NewPin = () => { ); }; +const Wrapper = styled(Flex)` + margin: 0 auto; + + @media (max-width: 1076px) { + width: calc(50vw - 40px); + } + + @media (max-width: 744px) { + width: ${({ width }) => width}; + margin: 0 auto; + } +`; + const ModalContentsWrapper = styled.div` width: 100%; height: 100%; diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index 30ef43a1..8303ffb4 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -17,6 +17,7 @@ import { ModalContext } from '../context/ModalContext'; import AddToMyTopicList from '../components/ModalMyTopicList/addToMyTopicList'; interface PinDetailProps { + width: '372px' | '100vw'; topicId: string; pinId: number; isEditPinDetail: boolean; @@ -26,6 +27,7 @@ interface PinDetailProps { const userToken = localStorage.getItem('userToken'); const PinDetail = ({ + width, topicId, pinId, isEditPinDetail, @@ -57,24 +59,20 @@ const PinDetail = ({ showToast('error', '로그인 후 사용해주세요.'); }; - useEffect(() => { - const getPinData = async () => { - const pinData = await getApi(`/pins/${pinId}`); - setPin(pinData); - setFormValues({ - name: pinData.name, - images: pinData.images, - description: pinData.description, - }); - }; + const getPinData = async () => { + const pinData = await getApi(`/pins/${pinId}`); + setPin(pinData); + setFormValues({ + name: pinData.name, + images: pinData.images, + description: pinData.description, + }); + }; + useEffect(() => { getPinData(); }, [pinId, searchParams]); - const updateQueryString = (key: string, value: string) => { - setSearchParams({ ...Object.fromEntries(searchParams), [key]: value }); - }; - const onClickEditPin = () => { setIsEditPinDetail(true); setErrorMessages({ @@ -82,7 +80,6 @@ const PinDetail = ({ images: '', description: '', }); - updateQueryString('edit', 'true'); }; const copyContent = async () => { @@ -98,19 +95,22 @@ const PinDetail = ({ if (isEditPinDetail) return ( - + + + ); return ( - <> + {pin.name} @@ -184,20 +184,24 @@ const PinDetail = ({ + + 내 지도에 저장하기 - + 공유하기 + + @@ -213,10 +217,45 @@ const PinDetail = ({ - + ); }; +const Wrapper = styled.section<{ + $layoutWidth: '372px' | '100vw'; + $selectedPinId: number | null; +}>` + display: flex; + flex-direction: column; + width: ${({ $layoutWidth }) => $layoutWidth}; + height: 100vh; + overflow: auto; + position: absolute; + top: 0; + left: ${({ $layoutWidth }) => $layoutWidth}; + padding: ${({ theme }) => theme.spacing[4]}; + border-left: 1px solid ${({ theme }) => theme.color.gray}; + background-color: ${({ theme }) => theme.color.white}; + z-index: 1; + + @media (max-width: 1076px) { + width: 50vw; + margin-top: 50vh; + height: ${({ $layoutWidth }) => $layoutWidth === '372px' && '50vh'}; + left: ${({ $selectedPinId }) => $selectedPinId && '50vw'}; + } + + @media (max-width: 744px) { + border-left: 0; + left: 0; + width: 100vw; + } + + @media (max-width: 372px) { + width: ${({ $layoutWidth }) => $layoutWidth}; + } +`; + const PinDetailImgContainer = styled(Flex)` box-shadow: 8px 8px 8px 0px rgba(69, 69, 69, 0.15); `; @@ -251,8 +290,7 @@ const ButtonsWrapper = styled.div` align-items: center; width: 332px; height: 48px; - position: fixed; - bottom: 24px; + margin: 0 auto; `; export default PinDetail; diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index c2a76b89..abafac26 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -10,6 +10,7 @@ import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleto import { Suspense, lazy } from 'react'; import Text from '../components/common/Text'; import useNavigator from '../hooks/useNavigator'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicCardList = lazy(() => import('../components/TopicCardList')); @@ -70,6 +71,8 @@ const Profile = () => { const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; + + ${setFullScreenResponsive()} `; const MyInfoWrapper = styled(Flex)` diff --git a/frontend/src/pages/RootPage.tsx b/frontend/src/pages/RootPage.tsx index a51b16aa..de650e4a 100644 --- a/frontend/src/pages/RootPage.tsx +++ b/frontend/src/pages/RootPage.tsx @@ -1,14 +1,17 @@ import { Outlet } from 'react-router-dom'; import Layout from '../components/Layout'; import LayoutWidthProvider from '../context/LayoutWidthContext'; +import NavbarHighlightsProvider from '../context/NavbarHighlightsContext'; const RootPage = () => { return ( <> - - - + + + + + ); diff --git a/frontend/src/pages/SeeAllLatestTopics.tsx b/frontend/src/pages/SeeAllLatestTopics.tsx index 4cd33932..e949f1a4 100644 --- a/frontend/src/pages/SeeAllLatestTopics.tsx +++ b/frontend/src/pages/SeeAllLatestTopics.tsx @@ -8,6 +8,7 @@ import Box from '../components/common/Box'; import { Suspense, lazy } from 'react'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import useNavigator from '../hooks/useNavigator'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicCardList = lazy(() => import('../components/TopicCardList')); @@ -45,6 +46,8 @@ const SeeAllLatestTopics = () => { const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; + + ${setFullScreenResponsive()} `; export default SeeAllLatestTopics; diff --git a/frontend/src/pages/SeeAllNearTopics.tsx b/frontend/src/pages/SeeAllNearTopics.tsx index 510b2f8e..15cd3035 100644 --- a/frontend/src/pages/SeeAllNearTopics.tsx +++ b/frontend/src/pages/SeeAllNearTopics.tsx @@ -8,6 +8,7 @@ import Text from '../components/common/Text'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import { Suspense, lazy } from 'react'; import useNavigator from '../hooks/useNavigator'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicCardList = lazy(() => import('../components/TopicCardList')); @@ -45,6 +46,8 @@ const SeeAllNearTopics = () => { const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; + + ${setFullScreenResponsive()} `; export default SeeAllNearTopics; diff --git a/frontend/src/pages/SeeAllPopularTopics.tsx b/frontend/src/pages/SeeAllPopularTopics.tsx index 0f1a717d..d40d25c0 100644 --- a/frontend/src/pages/SeeAllPopularTopics.tsx +++ b/frontend/src/pages/SeeAllPopularTopics.tsx @@ -8,6 +8,7 @@ import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import { Suspense, lazy } from 'react'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import useNavigator from '../hooks/useNavigator'; +import { setFullScreenResponsive } from '../constants/responsive'; const TopicCardList = lazy(() => import('../components/TopicCardList')); @@ -45,6 +46,8 @@ const SeeAllTopics = () => { const Wrapper = styled(Box)` width: 1036px; margin: 0 auto; + + ${setFullScreenResponsive()} `; export default SeeAllTopics; diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index ec63bb2a..60b7b700 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -8,10 +8,8 @@ import { } from 'react'; import { styled } from 'styled-components'; import Space from '../components/common/Space'; -import Flex from '../components/common/Flex'; import { TopicDetailProps } from '../types/Topic'; import { useParams, useSearchParams } from 'react-router-dom'; -import theme from '../themes'; import PinDetail from './PinDetail'; import { getApi } from '../apis/getApi'; import PullPin from '../components/PullPin'; @@ -92,6 +90,8 @@ const SelectedTopic = () => { const queryParams = new URLSearchParams(location.search); if (queryParams.has('pinDetail')) { setSelectedPinId(Number(queryParams.get('pinDetail'))); + } else { + setSelectedPinId(null); } setIsOpen(true); @@ -109,77 +109,86 @@ const SelectedTopic = () => { if (!topicId) return <>; return ( - <> - - - {tags.length > 0 && ( - - )} - }> - {topicDetails.map((topicDetail, idx) => ( - - - {idx !== topicDetails.length - 1 ? : <>} - - ))} - - - {selectedPinId && ( - <> - - ◀ - - - - - - - - )} - - + + + {tags.length > 0 && ( + + )} + }> + {topicDetails.map((topicDetail, idx) => ( + + + {idx !== topicDetails.length - 1 ? : <>} + + ))} + + + {selectedPinId && ( + <> + + ◀ + + + + + + )} + ); }; +const Wrapper = styled.section<{ + width: 'calc(100vw - 40px)' | 'calc(372px - 40px)'; + $selectedPinId: number | null; +}>` + display: flex; + flex-direction: column; + width: ${({ width }) => width}; + margin: ${({ $selectedPinId }) => $selectedPinId === null && '0 auto'}; + + @media (max-width: 1076px) { + width: calc(50vw - 40px); + } + + @media (max-width: 744px) { + width: 100%; + } + + @media (max-width: 372px) { + width: ${({ width }) => width}; + } +`; + const PinDetailWrapper = styled.div` &.collapsedPinDetail { z-index: -1; } `; -const ToggleButton = styled.button<{ $isCollapsed: boolean }>` +const ToggleButton = styled.button<{ + $isCollapsed: boolean; +}>` position: absolute; top: 50%; left: 744px; @@ -204,6 +213,10 @@ const ToggleButton = styled.button<{ $isCollapsed: boolean }>` &:hover { background-color: #f5f5f5; } + + @media (max-width: 1076px) { + display: none; + } `; export default SelectedTopic; diff --git a/frontend/src/pages/UpdatedPinDetail.tsx b/frontend/src/pages/UpdatedPinDetail.tsx index cc147348..0d43be33 100644 --- a/frontend/src/pages/UpdatedPinDetail.tsx +++ b/frontend/src/pages/UpdatedPinDetail.tsx @@ -1,13 +1,14 @@ import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; import Text from '../components/common/Text'; -import Box from '../components/common/Box'; import { putApi } from '../apis/putApi'; import { SetURLSearchParams } from 'react-router-dom'; import { ModifyPinFormProps } from '../types/FormValues'; import InputContainer from '../components/InputContainer'; import { hasErrorMessage, hasNullValue } from '../validations'; import useToast from '../hooks/useToast'; +import Button from '../components/common/Button'; +import styled from 'styled-components'; interface UpdatedPinDetailProps { searchParams: URLSearchParams; @@ -15,6 +16,7 @@ interface UpdatedPinDetailProps { formValues: ModifyPinFormProps; errorMessages: Record; setSearchParams: SetURLSearchParams; + updatePinDetailAfterEditing: () => void; setIsEditing: React.Dispatch>; onChangeInput: ( e: React.ChangeEvent, @@ -30,6 +32,7 @@ const UpdatedPinDetail = ({ errorMessages, setSearchParams, setIsEditing, + updatePinDetailAfterEditing, onChangeInput, }: UpdatedPinDetailProps) => { const { showToast } = useToast(); @@ -49,6 +52,7 @@ const UpdatedPinDetail = ({ await putApi(`/pins/${pinId}`, formValues); setIsEditing(false); removeQueryString('edit'); + updatePinDetailAfterEditing(); showToast('info', '핀 수정을 완료하였습니다.'); } catch (error) { @@ -62,7 +66,7 @@ const UpdatedPinDetail = ({ }; return ( - + - + - + - - - 취소 - - - - - - 저장 - - + + + - + + + ); }; +const Wrapper = styled.div` + margin: 0 auto; + + @media (max-width: 1076px) { + width: calc(50vw - 40px); + } + + @media (max-width: 744px) { + width: 332px; + margin: 0 auto; + } +`; + export default UpdatedPinDetail; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index ca5a572b..ce56ae7a 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,7 +7,7 @@ import SelectedTopic from './pages/SelectedTopic'; import SeeAllPopularTopics from './pages/SeeAllPopularTopics'; import SeeAllNearTopics from './pages/SeeAllNearTopics'; import SeeAllLatestTopics from './pages/SeeAllLatestTopics'; -import KakaoRedirectPage from './pages/KakaoRedirectPage'; +import KakaoRedirect from './pages/KakaoRedirect'; import { ReactNode } from 'react'; import AuthLayout from './components/Layout/AuthLayout'; import NotFound from './pages/NotFound'; @@ -90,7 +90,7 @@ const routes: routeElement[] = [ }, { path: '/oauth/redirected/kakao', - element: , + element: , withAuth: false, }, ], From 6f60cacbce50efd10ad975dd9dfbbf54f944b449 Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:38:45 +0900 Subject: [PATCH 06/53] =?UTF-8?q?[FE]=20Refactor/#395=20=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=83=80=EC=9E=85=20=EC=A7=80=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20Map=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tmap api 관련 타입을 선언한다 * refactor: Layout 컴포넌트에서 지도에 관한 관심사를 분리한다 * refactor: Tmap 관련 타입 추가 * refactor: Tmap 관련 타입 적용 * refactor: 불필요한 코드 제거 * refactor: 좌표관련 타입 수정 * refactor: Navbar 클릭시 색 변경 로직 변경 및 타입 수정 * refactor: null 체크 * refactor: 타입 단언 수정 * refactor: null 타입 체크 --- frontend/package-lock.json | 23 ++--- frontend/src/components/Layout/index.tsx | 65 +++++++------- frontend/src/components/Map/index.tsx | 42 ++++++--- frontend/src/components/TopicInfo/index.tsx | 7 -- frontend/src/context/CoordinatesContext.tsx | 15 ++-- frontend/src/context/MarkerContext.tsx | 85 +++++++++--------- .../src/context/NavbarHighlightsContext.tsx | 51 +++++------ frontend/src/hooks/useAnimateClickedPin.ts | 9 +- frontend/src/hooks/useClickedCoordinate.ts | 5 +- frontend/src/hooks/useFocusToMarkers.ts | 13 +-- frontend/src/hooks/useMapClick.ts | 4 +- frontend/src/hooks/useSetNavbarHighlight.ts | 87 +++---------------- frontend/src/hooks/useUpdateCoordinates.ts | 8 +- frontend/src/pages/Home.tsx | 2 +- frontend/src/pages/NewPin.tsx | 2 +- frontend/src/pages/NewTopic.tsx | 2 +- frontend/src/pages/PinDetail.tsx | 5 +- frontend/src/pages/SelectedTopic.tsx | 2 +- frontend/src/pages/UpdatedPinDetail.tsx | 2 +- frontend/src/types/FormValues.ts | 37 -------- frontend/src/types/index.d.ts | 10 +-- frontend/src/types/tmap.d.ts | 65 ++++++++++++++ frontend/tsconfig.json | 7 +- 23 files changed, 259 insertions(+), 289 deletions(-) delete mode 100644 frontend/src/types/FormValues.ts create mode 100644 frontend/src/types/tmap.d.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba9f5d18..ddbb5aae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapbefine", - "version": "1.0.0", + "version": "0.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapbefine", - "version": "1.0.0", + "version": "0.8.0", "license": "ISC", "dependencies": { "@types/react-router-dom": "^5.3.3", @@ -8858,8 +8858,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -8875,9 +8873,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -30430,14 +30426,15 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "requires": {}, + "requires": { + "ajv": "^8.0.0" + }, "dependencies": { "ajv": { - "version": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "optional": true, - "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -30449,9 +30446,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "optional": true, - "peer": true + "dev": true } } }, diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index f59d4f10..947cc297 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useRef, useState } from 'react'; +import { useContext } from 'react'; import Map from '../Map'; import Flex from '../common/Flex'; import Logo from './Logo'; @@ -20,35 +20,13 @@ type LayoutProps = { children: React.ReactNode; }; -declare global { - interface Window { - Tmapv3: any; - daum: any; - } -} - const Layout = ({ children }: LayoutProps) => { - const { Tmapv3 } = window; - const mapContainer = useRef(null); const { width } = useContext(LayoutWidthContext); const { navbarHighlights } = useContext(NavbarHighlightsContext); - - const [map, setMap] = useState(null); - - useEffect(() => { - const map = new Tmapv3.Map(mapContainer.current, { - center: new Tmapv3.LatLng(37.5154, 127.1029), - }); - map.setZoomLimit(7, 18); - setMap(map); - return () => { - map.destroy(); - }; - }, []); - return ( + @@ -76,17 +54,33 @@ const Layout = ({ children }: LayoutProps) => { overflow="auto" padding="0 20px 20px 20px" > - {children} - - - - - - - - - - + + + + + + + + {children} + + + + + + + + + + + ); @@ -128,6 +122,7 @@ const MediaWrapper = styled.section<{ } `; + const LayoutFlex = styled(Flex)<{ $layoutWidth: '372px' | '100vw' }>` transition: all ease 0.3s; diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx index 2305fd36..5c95e203 100644 --- a/frontend/src/components/Map/index.tsx +++ b/frontend/src/components/Map/index.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useContext } from 'react'; +import { useContext, useLayoutEffect, useRef, useState } from 'react'; import Flex from '../common/Flex'; import { MarkerContext } from '../../context/MarkerContext'; import useMapClick from '../../hooks/useMapClick'; @@ -9,24 +9,46 @@ import useAnimateClickedPin from '../../hooks/useAnimateClickedPin'; import { styled } from 'styled-components'; import { LayoutWidthContext } from '../../context/LayoutWidthContext'; -const Map = (props: any, ref: any) => { - const { map } = props; +const Map = () => { + const { Tmapv3 } = window; + const { markers } = useContext(MarkerContext); const { width } = useContext(LayoutWidthContext); + const [mapInstance, setMapInstance] = useState(null); + + const mapContainer = useRef(null); + + useLayoutEffect(() => { + if (!Tmapv3 || !mapContainer.current) return; + + const map = new Tmapv3.Map(mapContainer.current, { + center: new Tmapv3.LatLng(37.5154, 127.1029), + }); + + if (!map) return; + + map.setZoomLimit(7, 17); + + setMapInstance(map); + + return () => { + map.destroy(); + }; + }, []); - useMapClick(map); - useClickedCoordinate(map); - useUpdateCoordinates(map); + useMapClick(mapInstance); + useClickedCoordinate(mapInstance); + useUpdateCoordinates(mapInstance); - useFocusToMarker(map, markers); - useAnimateClickedPin(map, markers); + useFocusToMarker(mapInstance, markers); + useAnimateClickedPin(mapInstance, markers); return ( @@ -46,4 +68,4 @@ const MapFlex = styled(Flex)` } `; -export default forwardRef(Map); +export default Map; diff --git a/frontend/src/components/TopicInfo/index.tsx b/frontend/src/components/TopicInfo/index.tsx index 1b5f7f18..72bf4e59 100644 --- a/frontend/src/components/TopicInfo/index.tsx +++ b/frontend/src/components/TopicInfo/index.tsx @@ -2,7 +2,6 @@ import Flex from '../common/Flex'; import Text from '../common/Text'; import Image from '../common/Image'; import Space from '../common/Space'; -import useNavigator from '../../hooks/useNavigator'; import useToast from '../../hooks/useToast'; import SmallTopicPin from '../../assets/smallTopicPin.svg'; import SmallTopicStar from '../../assets/smallTopicStar.svg'; @@ -33,7 +32,6 @@ export interface TopicInfoProps { } const TopicInfo = ({ - fullUrl, topicId, idx, topicImage, @@ -47,13 +45,8 @@ const TopicInfo = ({ isBookmarked, setTopicsFromServer, }: TopicInfoProps) => { - const { routePage } = useNavigator(); const { showToast } = useToast(); - const goToNewPin = () => { - routePage(`/new-pin?topic-id=${topicId}`, fullUrl); - }; - const copyContent = async () => { try { const topicUrl = window.location.href.split('?')[0]; diff --git a/frontend/src/context/CoordinatesContext.tsx b/frontend/src/context/CoordinatesContext.tsx index 33d81b30..ab409078 100644 --- a/frontend/src/context/CoordinatesContext.tsx +++ b/frontend/src/context/CoordinatesContext.tsx @@ -7,9 +7,10 @@ import { } from 'react'; export interface Coordinate { - latitude: string; - longitude: string; + latitude: number; + longitude: number; address?: string; + topicId?: string; } export interface CoordinatesContextType { @@ -22,7 +23,7 @@ export interface CoordinatesContextType { export const CoordinatesContext = createContext({ coordinates: [], setCoordinates: () => {}, - clickedCoordinate: { latitude: '', longitude: '', address: '' }, + clickedCoordinate: { latitude: 0, longitude: 0, address: '' }, setClickedCoordinate: () => {}, }); @@ -32,18 +33,18 @@ interface Props { const CoordinatesProvider = ({ children }: Props): JSX.Element => { const [coordinates, setCoordinates] = useState([ - { latitude: '37.5055', longitude: '127.0509' }, + { latitude: 37.5055, longitude: 127.0509 }, ]); const [clickedCoordinate, setClickedCoordinate] = useState({ - latitude: '', - longitude: '', + latitude: 0, + longitude: 0, address: '', }); // new-pin페이지가 아닌 경우 address를 =''로 고정 useEffect(() => { if (location.pathname !== '/new-pin') { - setClickedCoordinate({ ...clickedCoordinate, address: '' }); + setClickedCoordinate((prevState) => ({ ...prevState, address: '' })); } }, [location.pathname]); diff --git a/frontend/src/context/MarkerContext.tsx b/frontend/src/context/MarkerContext.tsx index 362ad41a..11adc7f3 100644 --- a/frontend/src/context/MarkerContext.tsx +++ b/frontend/src/context/MarkerContext.tsx @@ -1,27 +1,31 @@ import { createContext, useContext, useState } from 'react'; -import { CoordinatesContext } from './CoordinatesContext'; +import { Coordinate, CoordinatesContext } from './CoordinatesContext'; import { useParams } from 'react-router-dom'; import useNavigator from '../hooks/useNavigator'; import { pinColors, pinImageMap } from '../constants/pinImage'; type MarkerContextType = { - markers: any[]; - clickedMarker: any; - createMarkers: (map: any) => void; + markers: Marker[]; + clickedMarker: Marker | null; + createMarkers: (map: TMap) => void; removeMarkers: () => void; removeInfowindows: () => void; - createInfowindows: (map: any) => void; - displayClickedMarker: (map: any) => void; + createInfowindows: (map: TMap) => void; + displayClickedMarker: (map: TMap) => void; +}; + +const defaultMarkerContext = () => { + throw new Error('MarkerContext가 제공되지 않았습니다.'); }; export const MarkerContext = createContext({ markers: [], clickedMarker: null, - createMarkers: () => {}, - removeMarkers: () => {}, - displayClickedMarker: () => {}, - removeInfowindows: () => {}, - createInfowindows: () => {}, + createMarkers: defaultMarkerContext, + removeMarkers: defaultMarkerContext, + removeInfowindows: defaultMarkerContext, + createInfowindows: defaultMarkerContext, + displayClickedMarker: defaultMarkerContext, }); interface Props { @@ -29,20 +33,33 @@ interface Props { } const MarkerProvider = ({ children }: Props): JSX.Element => { - const [markers, setMarkers] = useState([]); - const [infoWindows, setInfoWindows] = useState([]); - const [clickedMarker, setClickedMarker] = useState(null); + const { Tmapv3 } = window; + const [markers, setMarkers] = useState([]); + const [infoWindows, setInfoWindows] = useState(null); + const [clickedMarker, setClickedMarker] = useState(null); const { coordinates, clickedCoordinate } = useContext(CoordinatesContext); const { topicId } = useParams<{ topicId: string }>(); const { routePage } = useNavigator(); + const createMarker = ( + coordinate: Coordinate, + map: TMap, + markerType: number, + ) => { + return new Tmapv3.Marker({ + position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), + icon: pinImageMap[markerType + 1], + map, + }); + }; + // 현재 클릭된 좌표의 마커 생성 - const displayClickedMarker = (map: any) => { + const displayClickedMarker = (map: TMap) => { if (clickedMarker) { clickedMarker.setMap(null); } - const marker = new window.Tmapv3.Marker({ - position: new window.Tmapv3.LatLng( + const marker = new Tmapv3.Marker({ + position: new Tmapv3.LatLng( clickedCoordinate.latitude, clickedCoordinate.longitude, ), @@ -54,40 +71,29 @@ const MarkerProvider = ({ children }: Props): JSX.Element => { }; //coordinates를 받아서 marker를 생성하고, marker를 markers 배열에 추가 - const createMarkers = (map: any) => { + const createMarkers = (map: TMap) => { let markerType = -1; let currentTopicId = '-1'; - const newMarkers = coordinates.map((coordinate: any) => { - // coordinate.topicId를 나누기 7한 나머지를 문자열로 변환 + let newMarkers = coordinates.map((coordinate: any) => { if (currentTopicId !== coordinate.topicId) { markerType = (markerType + 1) % 7; currentTopicId = coordinate.topicId; } - - const marker = new window.Tmapv3.Marker({ - position: new window.Tmapv3.LatLng( - coordinate.latitude, - coordinate.longitude, - ), - icon: pinImageMap[markerType + 1], - map, - }); + let marker = createMarker(coordinate, map, markerType); marker.id = String(coordinate.id); return marker; }); - //newMarkers 각각에 onClick 이벤트를 추가 - newMarkers.forEach((marker: any) => { + newMarkers.forEach((marker: Marker) => { marker.on('click', () => { routePage(`/topics/${topicId}?pinDetail=${marker.id}`); }); }); - setMarkers(newMarkers); }; - const createInfowindows = (map: any) => { + const createInfowindows = (map: TMap) => { let markerType = -1; let currentTopicId = '-1'; @@ -97,16 +103,13 @@ const MarkerProvider = ({ children }: Props): JSX.Element => { currentTopicId = coordinate.topicId; } - const infoWindow = new window.Tmapv3.InfoWindow({ - position: new window.Tmapv3.LatLng( - coordinate.latitude, - coordinate.longitude, - ), + const infoWindow = new Tmapv3.InfoWindow({ + position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), content: `
${coordinate.pinName}
`, - offset: new window.Tmapv3.Point(0, -60), + offset: new Tmapv3.Point(0, -60), type: 2, map: map, }); @@ -117,12 +120,12 @@ const MarkerProvider = ({ children }: Props): JSX.Element => { }; const removeMarkers = () => { - markers.forEach((marker: any) => marker.setMap(null)); + markers?.forEach((marker: Marker) => marker.setMap(null)); setMarkers([]); }; const removeInfowindows = () => { - infoWindows.forEach((infowindow: any) => infowindow.setMap(null)); + infoWindows?.forEach((infoWindow: InfoWindow) => infoWindow.setMap(null)); setInfoWindows([]); }; diff --git a/frontend/src/context/NavbarHighlightsContext.tsx b/frontend/src/context/NavbarHighlightsContext.tsx index 8d71d079..95db3879 100644 --- a/frontend/src/context/NavbarHighlightsContext.tsx +++ b/frontend/src/context/NavbarHighlightsContext.tsx @@ -6,39 +6,39 @@ import { useState, } from 'react'; -interface NavbarHighlightsProps { - home: boolean; - seeTogether: boolean; - addMapOrPin: boolean; - favorite: boolean; - profile: boolean; -} +export type NavbarHighlightKeys = + | 'home' + | 'seeTogether' + | 'addMapOrPin' + | 'favorite' + | 'profile'; -interface NavbarHighlightsContextProps { - navbarHighlights: NavbarHighlightsProps; - setNavbarHighlights: Dispatch>; -} +export type NavbarHighlights = { + [key in NavbarHighlightKeys]: boolean; +}; interface NavbarHighlightsProviderProps { children: ReactNode; } -export const NavbarHighlightsContext = - createContext({ - navbarHighlights: { - home: true, - seeTogether: false, - addMapOrPin: false, - favorite: false, - profile: false, - }, - setNavbarHighlights: () => {}, - }); +export const NavbarHighlightsContext = createContext<{ + navbarHighlights: NavbarHighlights; + setNavbarHighlights: Dispatch>; +}>({ + navbarHighlights: { + home: true, + seeTogether: false, + addMapOrPin: false, + favorite: false, + profile: false, + }, + setNavbarHighlights: () => {}, +}); const NavbarHighlightsProvider = ({ children, }: NavbarHighlightsProviderProps) => { - const [navbarHighlights, setNavbarHighlights] = useState({ + const [navbarHighlights, setNavbarHighlights] = useState({ home: true, seeTogether: false, addMapOrPin: false, @@ -48,10 +48,7 @@ const NavbarHighlightsProvider = ({ return ( {children} diff --git a/frontend/src/hooks/useAnimateClickedPin.ts b/frontend/src/hooks/useAnimateClickedPin.ts index 615912de..50e86dcc 100644 --- a/frontend/src/hooks/useAnimateClickedPin.ts +++ b/frontend/src/hooks/useAnimateClickedPin.ts @@ -1,18 +1,15 @@ import { useEffect } from 'react'; -const useAnimateClickedPin = (map: any, markers: any) => { +const useAnimateClickedPin = (map: TMap | null, markers: Marker[]) => { useEffect(() => { const queryParams = new URLSearchParams(location.search); if (queryParams.has('pinDetail')) { const pinId = queryParams.get('pinDetail'); - const marker = markers.find((marker: any) => marker.id === pinId); - if (marker) { + const marker = markers.find((marker: Marker) => marker.id === pinId); + if (marker && map) { map.setCenter(marker.getPosition()); map.setZoom(17); - // marker._marker_data.options.animation = - // window.Tmapv3.MarkerOptions.ANIMATE_FLICKER; - // marker._marker_data.options.animationLength = 350; } } }, [markers, map]); diff --git a/frontend/src/hooks/useClickedCoordinate.ts b/frontend/src/hooks/useClickedCoordinate.ts index 874cc889..00eafe57 100644 --- a/frontend/src/hooks/useClickedCoordinate.ts +++ b/frontend/src/hooks/useClickedCoordinate.ts @@ -2,7 +2,8 @@ import { useContext, useEffect } from 'react'; import { CoordinatesContext } from '../context/CoordinatesContext'; import { MarkerContext } from '../context/MarkerContext'; -export default function useClickedCoordinate(map: any) { +export default function useClickedCoordinate(map: TMap | null) { + const { Tmapv3 } = window; const { clickedCoordinate } = useContext(CoordinatesContext); const { displayClickedMarker } = useContext(MarkerContext); @@ -13,7 +14,7 @@ export default function useClickedCoordinate(map: any) { // 선택된 좌표가 있으면 해당 좌표로 지도의 중심을 이동 if (clickedCoordinate.latitude && clickedCoordinate.longitude) { map.panTo( - new window.Tmapv3.LatLng( + new Tmapv3.LatLng( clickedCoordinate.latitude, clickedCoordinate.longitude, ), diff --git a/frontend/src/hooks/useFocusToMarkers.ts b/frontend/src/hooks/useFocusToMarkers.ts index 30e28714..a0aa7704 100644 --- a/frontend/src/hooks/useFocusToMarkers.ts +++ b/frontend/src/hooks/useFocusToMarkers.ts @@ -1,16 +1,17 @@ import { useEffect, useRef } from 'react'; -const useFocusToMarker = (map: any, markers: any) => { - const bounds = useRef(new window.Tmapv3.LatLngBounds()); +const useFocusToMarker = (map: TMap | null, markers: Marker[]) => { + const { Tmapv3 } = window; + const bounds = useRef(new Tmapv3.LatLngBounds()); useEffect(() => { - if (markers.length === 1) { + if (map && markers && markers.length === 1) { map.panTo(markers[0].getPosition()); } - if (markers.length > 1) { - bounds.current = new window.Tmapv3.LatLngBounds(); - markers.forEach((marker: any) => { + if (map && markers && markers.length > 1) { + bounds.current = new Tmapv3.LatLngBounds(); + markers.forEach((marker: Marker) => { bounds.current.extend(marker.getPosition()); }); diff --git a/frontend/src/hooks/useMapClick.ts b/frontend/src/hooks/useMapClick.ts index c0f67dfd..0e8dfdc8 100644 --- a/frontend/src/hooks/useMapClick.ts +++ b/frontend/src/hooks/useMapClick.ts @@ -3,11 +3,11 @@ import { CoordinatesContext } from '../context/CoordinatesContext'; import getAddressFromServer from '../lib/getAddressFromServer'; import useToast from './useToast'; -export default function useMapClick(map: any) { +export default function useMapClick(map: TMap | null) { const { setClickedCoordinate } = useContext(CoordinatesContext); const { showToast } = useToast(); - const clickHandler = async (evt: any) => { + const clickHandler = async (evt: evt) => { try { const roadName = await getAddressFromServer( evt.data.lngLat._lat, diff --git a/frontend/src/hooks/useSetNavbarHighlight.ts b/frontend/src/hooks/useSetNavbarHighlight.ts index b45fecd0..6a888765 100644 --- a/frontend/src/hooks/useSetNavbarHighlight.ts +++ b/frontend/src/hooks/useSetNavbarHighlight.ts @@ -1,7 +1,11 @@ import { useContext, useEffect } from 'react'; -import { NavbarHighlightsContext } from '../context/NavbarHighlightsContext'; +import { + NavbarHighlightKeys, + NavbarHighlights, + NavbarHighlightsContext, +} from '../context/NavbarHighlightsContext'; -const navbarPageNames = [ +const navbarPageNames: NavbarHighlightKeys[] = [ 'home', 'seeTogether', 'addMapOrPin', @@ -9,84 +13,19 @@ const navbarPageNames = [ 'profile', ]; -const useSetNavbarHighlight = (pageName: string) => { +const useSetNavbarHighlight = (pageName: NavbarHighlightKeys) => { const { navbarHighlights, setNavbarHighlights } = useContext( NavbarHighlightsContext, ); useEffect(() => { - if (!navbarPageNames.includes(pageName)) { - setNavbarHighlights({ - home: false, - seeTogether: false, - addMapOrPin: false, - favorite: false, - profile: false, - }); + const newNavbarHighlights: NavbarHighlights = navbarPageNames.reduce( + (acc, curr) => ({ ...acc, [curr]: curr === pageName }), + {} as NavbarHighlights, + ); - return; - } - - if (pageName === 'home') { - setNavbarHighlights({ - home: true, - seeTogether: false, - addMapOrPin: false, - favorite: false, - profile: false, - }); - - return; - } - - if (pageName === 'seeTogether') { - setNavbarHighlights({ - home: false, - seeTogether: true, - addMapOrPin: false, - favorite: false, - profile: false, - }); - - return; - } - - if (pageName === 'addMapOrPin') { - setNavbarHighlights({ - home: false, - seeTogether: false, - addMapOrPin: true, - favorite: false, - profile: false, - }); - - return; - } - - if (pageName === 'favorite') { - setNavbarHighlights({ - home: false, - seeTogether: false, - addMapOrPin: false, - favorite: true, - profile: false, - }); - - return; - } - - if (pageName === 'profile') { - setNavbarHighlights({ - home: false, - seeTogether: false, - addMapOrPin: false, - favorite: false, - profile: true, - }); - - return; - } - }, []); + setNavbarHighlights(newNavbarHighlights); + }, [pageName]); return { navbarHighlights }; }; diff --git a/frontend/src/hooks/useUpdateCoordinates.ts b/frontend/src/hooks/useUpdateCoordinates.ts index 8e52bf9e..6bc33d86 100644 --- a/frontend/src/hooks/useUpdateCoordinates.ts +++ b/frontend/src/hooks/useUpdateCoordinates.ts @@ -2,11 +2,7 @@ import { useContext, useEffect } from 'react'; import { CoordinatesContext } from '../context/CoordinatesContext'; import { MarkerContext } from '../context/MarkerContext'; -interface UseUpdateCoordinatesProps { - map: any; -} - -export default function useUpdateCoordinates(map: UseUpdateCoordinatesProps) { +export default function useUpdateCoordinates(map: TMap | null) { const { coordinates } = useContext(CoordinatesContext); const { markers, @@ -18,7 +14,7 @@ export default function useUpdateCoordinates(map: UseUpdateCoordinatesProps) { useEffect(() => { if (!map) return; - if (markers.length > 0) { + if (markers && markers.length > 0) { removeMarkers(); removeInfowindows(); } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 748ca403..ee07737b 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -34,7 +34,7 @@ const Home = () => { }; useEffect(() => { - if (markers.length > 0) { + if (markers && markers.length > 0) { removeMarkers(); removeInfowindows(); } diff --git a/frontend/src/pages/NewPin.tsx b/frontend/src/pages/NewPin.tsx index dc486642..7f716880 100644 --- a/frontend/src/pages/NewPin.tsx +++ b/frontend/src/pages/NewPin.tsx @@ -8,7 +8,7 @@ import { FormEvent, useContext, useEffect, useState } from 'react'; import { getApi } from '../apis/getApi'; import { TopicCardProps } from '../types/Topic'; import useNavigator from '../hooks/useNavigator'; -import { NewPinFormProps } from '../types/FormValues'; +import { NewPinFormProps } from '../types/tmap'; import useFormValues from '../hooks/useFormValues'; import { MarkerContext } from '../context/MarkerContext'; import { CoordinatesContext } from '../context/CoordinatesContext'; diff --git a/frontend/src/pages/NewTopic.tsx b/frontend/src/pages/NewTopic.tsx index 5acb370e..e1c5a57d 100644 --- a/frontend/src/pages/NewTopic.tsx +++ b/frontend/src/pages/NewTopic.tsx @@ -5,7 +5,7 @@ import Space from '../components/common/Space'; import Button from '../components/common/Button'; import { postApi } from '../apis/postApi'; import useNavigator from '../hooks/useNavigator'; -import { NewTopicFormProps } from '../types/FormValues'; +import { NewTopicFormProps } from '../types/tmap'; import useFormValues from '../hooks/useFormValues'; import { useLocation } from 'react-router-dom'; import useToast from '../hooks/useToast'; diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index 8303ffb4..7207a25b 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -8,7 +8,7 @@ import { useSearchParams } from 'react-router-dom'; import Box from '../components/common/Box'; import UpdatedPinDetail from './UpdatedPinDetail'; import useFormValues from '../hooks/useFormValues'; -import { ModifyPinFormProps } from '../types/FormValues'; +import { ModifyPinFormProps } from '../types/tmap'; import useToast from '../hooks/useToast'; import Button from '../components/common/Button'; import Modal from '../components/Modal'; @@ -18,7 +18,6 @@ import AddToMyTopicList from '../components/ModalMyTopicList/addToMyTopicList'; interface PinDetailProps { width: '372px' | '100vw'; - topicId: string; pinId: number; isEditPinDetail: boolean; setIsEditPinDetail: React.Dispatch>; @@ -28,14 +27,12 @@ const userToken = localStorage.getItem('userToken'); const PinDetail = ({ width, - topicId, pinId, isEditPinDetail, setIsEditPinDetail, }: PinDetailProps) => { const [searchParams, setSearchParams] = useSearchParams(); const [pin, setPin] = useState(null); - const [selectedTopic, setSelectedTopic] = useState(null); //토픽이 없을 때 사용하는 변수 const { showToast } = useToast(); const { formValues, diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index 60b7b700..dd3b0437 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -37,7 +37,7 @@ const SelectedTopic = () => { const { setCoordinates } = useContext(CoordinatesContext); const { tags, setTags } = useContext(TagContext); const { width } = useSetLayoutWidth(SIDEBAR); - const { navbarHighlights: __ } = useSetNavbarHighlight(''); + const { navbarHighlights: __ } = useSetNavbarHighlight('home'); const getAndSetDataFromServer = async () => { const data = await getApi(`/topics/ids?ids=${topicId}`); diff --git a/frontend/src/pages/UpdatedPinDetail.tsx b/frontend/src/pages/UpdatedPinDetail.tsx index 0d43be33..8a66c593 100644 --- a/frontend/src/pages/UpdatedPinDetail.tsx +++ b/frontend/src/pages/UpdatedPinDetail.tsx @@ -3,7 +3,7 @@ import Space from '../components/common/Space'; import Text from '../components/common/Text'; import { putApi } from '../apis/putApi'; import { SetURLSearchParams } from 'react-router-dom'; -import { ModifyPinFormProps } from '../types/FormValues'; +import { ModifyPinFormProps } from '../types/tmap'; import InputContainer from '../components/InputContainer'; import { hasErrorMessage, hasNullValue } from '../validations'; import useToast from '../hooks/useToast'; diff --git a/frontend/src/types/FormValues.ts b/frontend/src/types/FormValues.ts deleted file mode 100644 index 3bec058a..00000000 --- a/frontend/src/types/FormValues.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface DefaultFormProps { - name: string; - address: string; - description: string; -} - -export interface NewTopicFormProps { - name: string; - description: string; - image: string; - topics: []; -} - -export interface ModifyPinFormProps { - name: string; - images: string[]; - description: string; -} - -export interface DefaultPinFormProps extends ModifyPinFormProps { - id: number; - address: string; - latitude: string; - longitude: string; - updatedAt: string; -} - -export interface NewPinFormProps { - topicId: number; - name: string; - images: string[]; - description: string; - address: string; - latitude: string; - longitude: string; - legalDongCode: string; -} diff --git a/frontend/src/types/index.d.ts b/frontend/src/types/index.d.ts index ee8901d0..990e1578 100644 --- a/frontend/src/types/index.d.ts +++ b/frontend/src/types/index.d.ts @@ -4,8 +4,8 @@ declare module '*.svg' { export default SVG; } -declare global { - interface Window { - Tmapv3: Tmapv3; - } -} +// declare global { +// interface Window { +// Tmapv3: Tmapv3; +// } +// } diff --git a/frontend/src/types/tmap.d.ts b/frontend/src/types/tmap.d.ts new file mode 100644 index 00000000..d591aefe --- /dev/null +++ b/frontend/src/types/tmap.d.ts @@ -0,0 +1,65 @@ +interface Window { + Tmapv3: { + Map: new (element: HTMLElement, options?: { center?: LatLng }) => TMap; + LatLng: new (lat: number, lng: number) => LatLng; + LatLngBounds: new () => LatLngBounds; + Marker: new (options?: MarkerOptions) => Marker; + InfoWindow: new (options?: InfoWindowOptions) => InfoWindow; + Point: new (x: number, y: number) => Point; + }; + daum: any; +} + +interface evt { + data: { + lngLat: { + _lat: number; + _lng: number; + }; + }; +} + +interface TMap { + setZoomLimit(minZoom: number, maxZoom: number): void; + destroy(): void; + panTo(latLng: LatLng): void; + fitBounds(bounds: LatLngBounds): void; + setCenter(latLng: LatLng): void; + setZoom(zoomLevel: number): void; + on(eventType: string, callback: (evt: evt) => void): void; + removeListener(eventType: string, callback: (evt: evt) => void): void; +} + +interface LatLng {} + +interface LatLngBounds { + extend(latLng: LatLng): void; +} + +interface Marker { + position?: LatLng; + icon?: string; + map?: Map; + id?: string; + getPosition(): LatLng; + on(eventType: string, callback: (evt: Event) => void): void; + setMap(mapOrNull?: Map | null): void; +} + +interface Point { + x: number; + y: number; +} + +interface InfoWindow { + position?: LatLng; + content?: string; + offset?: Point; + type?: number; + map?: Map; + setMap(mapOrNull?: Map | null): void; + setPosition(positionOrLatLng?: Position | LatLng): void; + setContent(contentOrString?: Content | string): void; + open(map?: Map, marker?: Marker, latlng?: LatLng): void; + close(): void; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5b8f8024..3c7d8fe6 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -14,5 +14,10 @@ "types": ["cypress"] }, "exclude": ["node_modules"], - "include": ["./src/**/*.tsx", "./src/**/*.ts", "cypress/**/*"] + "include": [ + "./src/**/*.tsx", + "./src/**/*.ts", + "cypress/**/*", + "src/types/tmap.d.ts" + ] } From 9c561237a1150f3a514abed959fbda9bb2a7e68c Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:59:16 +0900 Subject: [PATCH 07/53] =?UTF-8?q?fix:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Layout/index.tsx | 42 ++++++------------------ 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index 947cc297..39af46f4 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -26,7 +26,6 @@ const Layout = ({ children }: LayoutProps) => { return ( - @@ -54,33 +53,17 @@ const Layout = ({ children }: LayoutProps) => { overflow="auto" padding="0 20px 20px 20px" > - - - - - - - - {children} - - - - - + {children} +
+ - - - - - + + + + + + + ); @@ -92,7 +75,6 @@ const LogoWrapper = styled.section<{ width: 372px; display: flex; padding: 12px 20px 0 20px; - @media (max-width: 1076px) { ${({ $layoutWidth }) => $layoutWidth === '372px' && @@ -113,7 +95,6 @@ const MediaWrapper = styled.section<{ display: flex; width: 100vw; overflow: hidden; - @media (max-width: 1076px) { flex-direction: ${({ $isAddPage, $layoutWidth }) => { if ($isAddPage) return 'column'; @@ -122,14 +103,11 @@ const MediaWrapper = styled.section<{ } `; - const LayoutFlex = styled(Flex)<{ $layoutWidth: '372px' | '100vw' }>` transition: all ease 0.3s; - @media (max-width: 1076px) { height: ${({ $layoutWidth }) => $layoutWidth === '372px' && '50vh'}; transition: none; } `; - export default Layout; From 5a1d2e142455ad9f96859469eec69edad8fd0a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Fri, 15 Sep 2023 01:29:41 +0900 Subject: [PATCH 08/53] =?UTF-8?q?[FE]=20Test/#369=20Jest=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 초기값 타입 수정 및 에러 객체 반환 테스트 추가 * refactor: 일부 단어가 정상적인 단어 입력을 방해하는 오류 수정 * test: 유효성 검사 테스트 추가 * style: single quote 적용 --- frontend/babel.config.js | 10 +- .../src/__tests__/hooks/useFormValues.test.ts | 44 +++++- .../src/__tests__/hooks/validation.test.ts | 139 ++++++++++++++++++ frontend/src/validations/index.ts | 2 +- 4 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 frontend/src/__tests__/hooks/validation.test.ts diff --git a/frontend/babel.config.js b/frontend/babel.config.js index fda6611d..43dfc43a 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -1,15 +1,15 @@ module.exports = { presets: [ [ - "@babel/preset-env", + '@babel/preset-env', { targets: { - browsers: ["last 2 versions", "not dead", "not ie <= 11"], + browsers: ['last 2 versions', 'not dead', 'not ie <= 11'], }, }, ], - ["@babel/preset-react"], - "@babel/preset-typescript", + ['@babel/preset-react'], + '@babel/preset-typescript', ], - plugins: ["babel-plugin-styled-components"], + plugins: ['babel-plugin-styled-components'], }; diff --git a/frontend/src/__tests__/hooks/useFormValues.test.ts b/frontend/src/__tests__/hooks/useFormValues.test.ts index 00db0c23..877c6483 100644 --- a/frontend/src/__tests__/hooks/useFormValues.test.ts +++ b/frontend/src/__tests__/hooks/useFormValues.test.ts @@ -2,20 +2,52 @@ import { renderHook } from '@testing-library/react'; import useFormValues from '../../hooks/useFormValues'; import { act } from 'react-dom/test-utils'; -describe('useFormValues 테스트', () => { +interface FormValuesProps { + name: string; + description: string; +} + +describe('useFormValues 훅 초기화 및 수정 테스트', () => { test('매개변수로 받은 초기값을 정상적으로 반환하는지 확인한다.', () => { - const { result } = renderHook(() => useFormValues('토픽 이름')); + const { result } = renderHook(() => + useFormValues({ + name: '선릉', + description: '선릉이란 무엇일까요?', + }), + ); - expect(result.current.formValues).toBe('토픽 이름'); + expect(result.current.formValues.name).toBe('선릉'); }); test('setFormValues를 통해 set할 수 있는지 확인하다.', () => { - const { result } = renderHook(() => useFormValues('토픽 이름')); + const { result } = renderHook(() => + useFormValues({ + name: '잠실역 주변 맛집', + description: '선릉역 주변에서 먹을 만한 곳들 모음집입니다.', + }), + ); act(() => { - result.current.setFormValues('인기 있는 토픽'); + result.current.setFormValues((prevState) => ({ + ...prevState, + description: '잠실역 주변에서 맛있고 유명한 곳들을 모아봤습니다.', + })); }); - expect(result.current.formValues).toBe('인기 있는 토픽'); + expect(result.current.formValues.description).toBe( + '잠실역 주변에서 맛있고 유명한 곳들을 모아봤습니다.', + ); + }); + + test('입력 받은 초기값 타입 형식에 맞게 에러 객체를 반환하는지 확인한다.', () => { + const { result } = renderHook(() => + useFormValues({ + name: '혼자 돌기 좋은 서울 산책로', + description: + '조용하고 혼자 사색에 잠겨 산책할 수 있는 코스들 모음입니다.', + }), + ); + + expect(result.current.errorMessages).toEqual({ name: '', description: '' }); }); }); diff --git a/frontend/src/__tests__/hooks/validation.test.ts b/frontend/src/__tests__/hooks/validation.test.ts new file mode 100644 index 00000000..946cb7a8 --- /dev/null +++ b/frontend/src/__tests__/hooks/validation.test.ts @@ -0,0 +1,139 @@ +import { renderHook } from '@testing-library/react'; +import { + hasErrorMessage, + hasNullValue, + validateCurse, + validatePolitically, +} from '../../validations'; +import useFormValues from '../../hooks/useFormValues'; +import { act } from 'react-dom/test-utils'; + +interface FormValuesProps { + name: string; + description: string; +} + +describe('사용자 입력 유효성 검사 테스트', () => { + test('validateCure 함수는 사용자가 욕설을 입력할 경우 true를 반환한다.', () => { + const USER_INPUT = '개새끼'; + + const result = validateCurse(USER_INPUT); + + expect(result).toBe(true); + }); + + test('validateCure 함수는 사용자가 입력한 값 중에 욕설이 포함되어 있으면 true를 반환한다.', () => { + const USER_INPUT = '달동네 개쓰레기들 모음'; + + const result = validateCurse(USER_INPUT); + + expect(result).toBe(true); + }); + + test('validatePolitically 함수는 사용자가 사회적 문제의 단어를 입력한 경우 true를 반환한다.', () => { + const USER_INPUT = '월북'; + + const result = validatePolitically(USER_INPUT); + + expect(result).toBe(true); + }); + + test('validatePolitically 함수는 사용자가 입력한 갑 중에 사회적 문제의 단어가 포함되어 있으면 true를 반환한다.', () => { + const USER_INPUT = '괴뢰군 2023년 최신 루트'; + + const result = validatePolitically(USER_INPUT); + + expect(result).toBe(true); + }); + + test('hasErrorMessage 함수는 사용자가 입력한 값 중 하나라도 유효성 검사에 걸리면 true를 반환한다.', () => { + const { result } = renderHook(() => + useFormValues({ + name: '', + description: '', + }), + ); + const userInput = { + target: { + name: 'name', + value: '개새끼', + }, + } as React.ChangeEvent; + + act(() => { + result.current.onChangeInput(userInput, true, 20); + }); + + const hasErrorMessageResult = hasErrorMessage(result.current.errorMessages); + + expect(hasErrorMessageResult).toBe(true); + }); + + test('hasErrorMessage 함수는 필수값 항목을 입력하지 않았을 경우 true를 반환한다.', () => { + const { result } = renderHook(() => + useFormValues({ + name: '', + description: '', + }), + ); + const userInput = { + target: { + name: 'name', + value: '', + }, + } as React.ChangeEvent; + + act(() => { + result.current.onChangeInput(userInput, true, 20); + }); + + const hasErrorMessageResult = hasErrorMessage(result.current.errorMessages); + + expect(hasErrorMessageResult).toBe(true); + }); + + test('hasErrorMessage 함수는 필수값이 아닌 항목을 입력하지 않으면 false를 반환한다.', () => { + const { result } = renderHook(() => + useFormValues({ + name: '', + description: '', + }), + ); + const userInput = { + target: { + name: 'name', + value: '', + }, + } as React.ChangeEvent; + + act(() => { + result.current.onChangeInput(userInput, false, 20); + }); + + const hasErrorMessageResult = hasErrorMessage(result.current.errorMessages); + + expect(hasErrorMessageResult).toBe(false); + }); + + test('hasNullValue 함수는 사용자가 아무것도 입력하지 않았을 경우 true를 반환한다.', () => { + const formValues = { + name: '', + description: '', + }; + + const result = hasNullValue(formValues); + + expect(result).toBe(true); + }); + + test('hasNullValue 함수는 사용자가 지정한 키의 값은 빈 값으로 두어도 false를 반환한다.', () => { + const formValues = { + name: '', + description: '선릉역 맛집 리스트입니다.', + }; + + const result = hasNullValue(formValues, 'name'); + + expect(result).toBe(false); + }); +}); diff --git a/frontend/src/validations/index.ts b/frontend/src/validations/index.ts index 0d0c59c2..8faab7d3 100644 --- a/frontend/src/validations/index.ts +++ b/frontend/src/validations/index.ts @@ -1,7 +1,7 @@ const REG_EXP_CURSES = /(간나새끼|개새끼|개새|개쓰레기|개소리|개씨발|개씹|지랄|좆|남창|느금마|니미럴|니애미|니애비|똘추|따까리|미친새끼|미친놈|미친년|병신|븅딱|빠구리|빨통|뻐큐|쌍놈|썅놈|쌍년|썅년|쌍노무새끼|썅노무새끼|시발|씨바|씨발|시팔|씨팔|씨부랄|시부랄|씹년|씹새끼|씹새|씹창|애새끼|애미뒤진|애미 뒤진|애비뒤진|애비 뒤진|엠창|육변기|좆|지랄|제기랄|창녀|창남|창놈|호로)/; const REG_EXP_POLITICALLY = - /(괴뢰|빨갱이|왜놈|일베|조센징|쪽바리|짱깨|월북|매국노|메갈|섹스|쎅쓰|쎅스|섹쓰|자지|보지)/; + /(괴뢰|빨갱이|왜놈|일베|조센징|쪽바리|짱깨|월북|매국노|메갈|섹스|쎅쓰|쎅스|섹쓰)/; export const validateCurse = (userInput: string) => { return REG_EXP_CURSES.test(userInput); From 592eba05838caae4d822db885ca0eb67211bf5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:26:13 +0900 Subject: [PATCH 09/53] =?UTF-8?q?[FE]=20Refactor/#402=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EB=B0=8F=20=ED=95=80=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A7=80=EB=8F=84=20=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84=20(#410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 사용자가 선택한 권한 부여 인원을 확인할 수 있도록 표시하는 기능 추가 * feat: 토픽 권한 타입 추가 * refactor: 가독성 개선 및 불필요한 중복 로직 제거 이중 삼항연산자 등 가독성이 저하되는 부분과 불필요한 조건문, 요청로직 등을 모두 제거한다. * refactor: 의미없는 함수 리턴 제거 * refactor: 토픽 생성 페이지에서 권한 설정 컴포넌트 UI 및 로직 분리 * feat: 토픽 권한 수정 페이지 구현 및 기존에 권한 설정한 친구들 보여주도록 변경 * feat: 토픽 및 핀에 대해서 수정하기 권한이 있을 경우에만 표시하도록 변경하는 기능 추가 * fix: 에러 페이지 레이아웃 width 미설정 오류 수정 * refactor: 수정하기 버튼 클릭 시 오류 발생 문구 수정 * refactor: 토픽 정보 수정 시 이전의 권한 설정 값으로 조정되도록 변경 * refactor: 사용하지 않는 코드 제거 및 if 블록 설정 * refactor: 사용하지 않는 import 및 prop 제거 * refactor: api 요청 커스텀 훅 매개변수 변경 및 에러 전파 옵션 추가 * refactor: api 요청 커스텀훅 로직 변경에 따른 적용 * refactor: 불필요한 코드 제거 * refactor: 수정 후 topicInfo를 다시 불러오도록 변경 * fix: 잘못된 delete permission id 설정 수정 topicId가 아니라 permission id를 사용하도록 변경한다. 추가로 에러메세지를 수정한다. * refactor: 불필요한 console.log 제거 --- frontend/src/apiHooks/useDelete.ts | 17 +- frontend/src/apiHooks/usePost.ts | 18 +- frontend/src/apiHooks/usePut.ts | 18 +- .../AuthorityRadioContainer/index.tsx | 290 +++++++++++++++ frontend/src/components/Modal/index.tsx | 1 + .../ModalMyTopicList/addToMyTopicList.tsx | 15 +- frontend/src/components/PinsOfTopic/index.tsx | 3 +- .../components/TopicInfo/UpdatedTopicInfo.tsx | 177 +++++++++ frontend/src/components/TopicInfo/index.tsx | 64 +++- frontend/src/mocks/handlers.js | 1 - frontend/src/pages/NewTopic.tsx | 347 +++--------------- frontend/src/pages/NotFound.tsx | 3 + frontend/src/pages/PinDetail.tsx | 24 +- frontend/src/pages/UpdatedPinDetail.tsx | 25 +- frontend/src/types/Pin.ts | 1 + frontend/src/types/Topic.ts | 17 + 16 files changed, 671 insertions(+), 350 deletions(-) create mode 100644 frontend/src/components/AuthorityRadioContainer/index.tsx create mode 100644 frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx diff --git a/frontend/src/apiHooks/useDelete.ts b/frontend/src/apiHooks/useDelete.ts index 38f61d67..2792552c 100644 --- a/frontend/src/apiHooks/useDelete.ts +++ b/frontend/src/apiHooks/useDelete.ts @@ -4,17 +4,22 @@ import { ContentTypeType } from '../types/Api'; interface fetchDeleteProps { url: string; + errorMessage: string; contentType?: ContentTypeType; + onSuccess?: () => void; + isThrow?: boolean; } const useDelete = () => { const { showToast } = useToast(); - const fetchDelete = async ( - { url, contentType }: fetchDeleteProps, - errorMessage: string, - onSuccess: () => void, - ) => { + const fetchDelete = async ({ + url, + contentType, + errorMessage, + onSuccess, + isThrow, + }: fetchDeleteProps) => { try { await deleteApi(url, contentType); @@ -23,6 +28,8 @@ const useDelete = () => { } } catch (e) { showToast('error', errorMessage); + + if (isThrow) throw e; } }; diff --git a/frontend/src/apiHooks/usePost.ts b/frontend/src/apiHooks/usePost.ts index df63216a..73e1f4ad 100644 --- a/frontend/src/apiHooks/usePost.ts +++ b/frontend/src/apiHooks/usePost.ts @@ -6,16 +6,22 @@ interface fetchPostProps { url: string; payload: {}; contentType?: ContentTypeType; + errorMessage: string; + onSuccess?: () => void; + isThrow?: boolean; } const usePost = () => { const { showToast } = useToast(); - const fetchPost = async ( - { url, payload, contentType }: fetchPostProps, - errorMessage: string, - onSuccess?: () => void, - ) => { + const fetchPost = async ({ + url, + payload, + contentType, + errorMessage, + onSuccess, + isThrow, + }: fetchPostProps) => { try { const responseData = await postApi(url, payload, contentType); @@ -26,6 +32,8 @@ const usePost = () => { return responseData; } catch (e) { showToast('error', errorMessage); + + if (isThrow) throw e; } }; diff --git a/frontend/src/apiHooks/usePut.ts b/frontend/src/apiHooks/usePut.ts index 5316a6b6..a263afdb 100644 --- a/frontend/src/apiHooks/usePut.ts +++ b/frontend/src/apiHooks/usePut.ts @@ -5,17 +5,23 @@ import { ContentTypeType } from '../types/Api'; interface fetchPutProps { url: string; payload: {}; + errorMessage: string; contentType?: ContentTypeType; + onSuccess?: () => void; + isThrow?: boolean; } const usePut = () => { const { showToast } = useToast(); - const fetchPut = async ( - { url, payload, contentType }: fetchPutProps, - errorMessage: string, - onSuccess?: () => void, - ) => { + const fetchPut = async ({ + url, + payload, + contentType, + errorMessage, + onSuccess, + isThrow, + }: fetchPutProps) => { try { const responseData = await putApi(url, payload, contentType); @@ -26,6 +32,8 @@ const usePut = () => { return responseData; } catch (e) { showToast('error', errorMessage); + + if (isThrow) throw e; } }; diff --git a/frontend/src/components/AuthorityRadioContainer/index.tsx b/frontend/src/components/AuthorityRadioContainer/index.tsx new file mode 100644 index 00000000..f2bffd8c --- /dev/null +++ b/frontend/src/components/AuthorityRadioContainer/index.tsx @@ -0,0 +1,290 @@ +import styled from 'styled-components'; +import Text from '../common/Text'; +import Space from '../common/Space'; +import Flex from '../common/Flex'; +import { useContext, useEffect, useState } from 'react'; +import { TopicAuthorMember, TopicAuthorMemberWithId } from '../../types/Topic'; +import { ModalContext } from '../../context/ModalContext'; +import Box from '../common/Box'; +import Modal from '../Modal'; +import Button from '../common/Button'; +import Checkbox from '../common/CheckBox'; +import useGet from '../../apiHooks/useGet'; + +interface AuthorityRadioContainer { + isPrivate: boolean; + isAll: boolean; + authorizedMemberIds: number[]; + setIsPrivate: React.Dispatch>; + setIsAll: React.Dispatch>; + setAuthorizedMemberIds: React.Dispatch>; + permissionedMembers?: TopicAuthorMemberWithId[]; +} + +const AuthorityRadioContainer = ({ + isPrivate, + isAll, + authorizedMemberIds, + setIsPrivate, + setIsAll, + setAuthorizedMemberIds, + permissionedMembers, +}: AuthorityRadioContainer) => { + const { openModal, closeModal } = useContext(ModalContext); + const { fetchGet } = useGet(); + + const [members, setMembers] = useState([]); + + useEffect(() => { + fetchGet( + '/members', + '사용자 목록을 가져오는데 실패했습니다.', + (response) => { + setMembers(response); + }, + ); + }, []); + + const onChangeInitAuthMembers = () => { + setIsAll(false); + openModal('newTopic'); + setAuthorizedMemberIds([]); + }; + + const onChangeInitAuthMembersWithSetIsAll = () => { + setIsAll(true); + setAuthorizedMemberIds([]); + }; + + const onChangeMemberChecked = (isChecked: boolean, id: number) => { + setAuthorizedMemberIds((prev: TopicAuthorMember['id'][]) => + isChecked ? [...prev, id] : prev.filter((n: number) => n !== id), + ); + }; + + return ( + <> + + 지도 종류 + + + + + setIsPrivate(false)} + tabIndex={4} + /> + + + + + + + setIsPrivate(true)} + tabIndex={4} + /> + + + + + + + + + 핀 생성 및 수정 권한 부여 + + + + + + + + + {isPrivate ? ( + + ) : ( + + )} + + + + + { + isAll === false && openModal('newTopic'); + }} + tabIndex={5} + /> + + + + + {authorizedMemberIds.length > 0 && ( + <> + + + + + 선택한 친구들 + + + {members.map((member) => { + if (authorizedMemberIds.includes(member.id)) + return ( + + • {member.nickName} + + ); + })} + + + )} + + {authorizedMemberIds.length === 0 && permissionedMembers && ( + <> + + + + + 기존에 선택한 친구들 + + + {permissionedMembers.map((member) => ( + + • {member.memberResponse.nickName} + + ))} + + + )} + + + + + + 멤버 선택 + + + {authorizedMemberIds.length}명 선택됨 + + + + + {members.map((member) => ( + + + + ))} + + + + + + + + + + + + + + + + ); +}; + +const ModalContentsWrapper = styled.div` + width: 100%; + height: 100%; + background-color: white; + display: flex; + flex-direction: column; +`; + +const CheckboxList = styled.div` + flex: 1; + overflow-y: scroll; +`; + +const CheckboxListItem = styled.div` + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 1rem; + padding: 1rem; + border-radius: 5px; + background-color: white; + + &:last-child { + margin-bottom: 0; + border-bottom: none; + } + + &:hover { + background-color: #f8f9fa; + } +`; + +export default AuthorityRadioContainer; diff --git a/frontend/src/components/Modal/index.tsx b/frontend/src/components/Modal/index.tsx index d642c6f3..645ed2a4 100644 --- a/frontend/src/components/Modal/index.tsx +++ b/frontend/src/components/Modal/index.tsx @@ -133,6 +133,7 @@ const Wrapper = styled.div` @media (max-width: 744px) { ${getModalPosition('bottom')}; width: 100%; + height: inherit; } `; diff --git a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx index 08dcafbd..a62aa88f 100644 --- a/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx +++ b/frontend/src/components/ModalMyTopicList/addToMyTopicList.tsx @@ -46,17 +46,16 @@ const AddToMyTopicList = ({ pin }: any) => { legalDongCode: '', }; - fetchPost( - { - url, - payload, - }, - '내 지도에 핀 추가를 실패하였습니다. 잠시 후 다시 시도해주세요.', - () => { + fetchPost({ + url, + payload, + errorMessage: + '내 지도에 핀 추가를 실패하였습니다. 잠시 후 다시 시도해주세요.', + onSuccess: () => { closeModal('addToMyTopicList'); showToast('info', '내 지도에 핀이 추가되었습니다.'); }, - ); + }); }; if (!myTopics) return <>; diff --git a/frontend/src/components/PinsOfTopic/index.tsx b/frontend/src/components/PinsOfTopic/index.tsx index 7a9bba83..64e9a586 100644 --- a/frontend/src/components/PinsOfTopic/index.tsx +++ b/frontend/src/components/PinsOfTopic/index.tsx @@ -1,5 +1,4 @@ import { styled } from 'styled-components'; -import { DEFAULT_TOPIC_IMAGE } from '../../constants'; import { TopicDetailProps } from '../../types/Topic'; import PinPreview from '../PinPreview'; import TopicInfo from '../TopicInfo'; @@ -24,7 +23,6 @@ const PinsOfTopic = ({ return ( >; +} + +interface FormValues { + name: string; + description: string; +} + +const UpdatedTopicInfo = ({ + id, + image, + name, + description, + setIsUpdate, +}: UpdatedTopicInfoProp) => { + const { fetchPost } = usePost(); + const { fetchGet } = useGet(); + const { fetchDelete } = useDelete(); + const { fetchPut } = usePut(); + const { showToast } = useToast(); + const { formValues, errorMessages, onChangeInput } = + useFormValues({ + name, + description, + }); + + const [topicAuthorInfo, setTopicAuthorInfo] = + useState(null); + const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 + const [isAll, setIsAll] = useState(true); // 모두 : 지정 인원 + const [authorizedMemberIds, setAuthorizedMemberIds] = useState([]); + + const updateTopicInfo = async () => { + try { + await fetchPut({ + url: `/topics/${id}`, + payload: { + name: formValues.name, + image, + description: formValues.description, + publicity: isPrivate ? 'PRIVATE' : 'PUBLIC', + permissionType: isAll && !isPrivate ? 'ALL_MEMBERS' : 'GROUP_ONLY', + }, + errorMessage: `'공개 ➡️ 비공개', '모두 ➡️ 친구들', '친구들 ➡️ 혼자' 로 변경할 수 없습니다.`, + isThrow: true, + }); + + if (authorizedMemberIds.length > 0) await updateTopicAuthority(); + + showToast('info', '지도를 수정하였습니다.'); + setIsUpdate(false); + } catch {} + }; + + const updateTopicAuthority = async () => { + // topicAuthorInfo api 구조 이상으로 권한 설정 자체에 대한 id를 사용 (topicId 아님) + await fetchDelete({ + url: `/permissions/${topicAuthorInfo?.permissionMembers[0].id}`, + errorMessage: '권한 삭제에 실패했습니다.', + isThrow: true, + }); + + await fetchPost({ + url: '/permissions', + payload: { + topicId: id, + memberIds: authorizedMemberIds, + }, + errorMessage: '권한 설정에 실패했습니다.', + isThrow: true, + }); + }; + + const cancelUpdateTopicInfo = () => { + setIsUpdate(false); + }; + + useEffect(() => { + fetchGet( + `/permissions/topics/${id}`, + '지도 권한 설정 정보를 가져오는데 실패했습니다.', + (response) => { + setTopicAuthorInfo(response); + setIsPrivate(response.publicity === 'PRIVATE'); + + if (topicAuthorInfo) { + setIsAll(topicAuthorInfo?.permissionMembers.length === 0); + } + }, + ); + }, []); + + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +const Wrapper = styled.section``; + +export default UpdatedTopicInfo; diff --git a/frontend/src/components/TopicInfo/index.tsx b/frontend/src/components/TopicInfo/index.tsx index 72bf4e59..d0118ca3 100644 --- a/frontend/src/components/TopicInfo/index.tsx +++ b/frontend/src/components/TopicInfo/index.tsx @@ -14,9 +14,11 @@ import { DEFAULT_TOPIC_IMAGE } from '../../constants'; import AddSeeTogether from '../AddSeeTogether'; import AddFavorite from '../AddFavorite'; import { styled } from 'styled-components'; +import Box from '../common/Box'; +import { useEffect, useState } from 'react'; +import UpdatedTopicInfo from './UpdatedTopicInfo'; export interface TopicInfoProps { - fullUrl?: string; topicId: string; idx: number; topicImage: string; @@ -26,6 +28,7 @@ export interface TopicInfoProps { topicPinCount: number; topicBookmarkCount: number; topicDescription: string; + canUpdate: boolean; isInAtlas: boolean; isBookmarked: boolean; setTopicsFromServer: () => void; @@ -41,12 +44,18 @@ const TopicInfo = ({ topicPinCount, topicBookmarkCount, topicDescription, + canUpdate, isInAtlas, isBookmarked, setTopicsFromServer, }: TopicInfoProps) => { + const [isUpdate, setIsUpdate] = useState(false); const { showToast } = useToast(); + const updateTopicInfo = () => { + setIsUpdate(true); + }; + const copyContent = async () => { try { const topicUrl = window.location.href.split('?')[0]; @@ -57,6 +66,22 @@ const TopicInfo = ({ } }; + useEffect(() => { + if (!isUpdate) setTopicsFromServer(); + }, [isUpdate]); + + if (isUpdate) { + return ( + + ); + } + return ( - - - - - - {topicPinCount > 999 ? '+999' : topicPinCount}개 - - - - - - - {topicBookmarkCount > 999 ? '+999' : topicBookmarkCount}명 - + + + + + + + {topicPinCount > 999 ? '+999' : topicPinCount}개 + + + + + + + {topicBookmarkCount > 999 ? '+999' : topicBookmarkCount}명 + + + {canUpdate && ( + + + 수정하기 + + + )} diff --git a/frontend/src/mocks/handlers.js b/frontend/src/mocks/handlers.js index cd13a5a0..be36c323 100644 --- a/frontend/src/mocks/handlers.js +++ b/frontend/src/mocks/handlers.js @@ -78,7 +78,6 @@ export const handlers = [ data = detailTopic.filter((topic) => Number(topic.id) === topicId); } - console.log(data); if (!data) { return res(ctx.status(403), ctx.json(data)); } diff --git a/frontend/src/pages/NewTopic.tsx b/frontend/src/pages/NewTopic.tsx index e1c5a57d..9227d645 100644 --- a/frontend/src/pages/NewTopic.tsx +++ b/frontend/src/pages/NewTopic.tsx @@ -1,9 +1,8 @@ -import { useContext, useEffect, useState } from 'react'; +import { useContext, useState } from 'react'; import Text from '../components/common/Text'; import Flex from '../components/common/Flex'; import Space from '../components/common/Space'; import Button from '../components/common/Button'; -import { postApi } from '../apis/postApi'; import useNavigator from '../hooks/useNavigator'; import { NewTopicFormProps } from '../types/tmap'; import useFormValues from '../hooks/useFormValues'; @@ -14,52 +13,30 @@ import { hasErrorMessage, hasNullValue } from '../validations'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { DEFAULT_TOPIC_IMAGE, LAYOUT_PADDING, SIDEBAR } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; -import Modal from '../components/Modal'; -import { styled } from 'styled-components'; -import { ModalContext } from '../context/ModalContext'; -import { getApi } from '../apis/getApi'; -import { MemberProps } from '../types/Login'; -import Checkbox from '../components/common/CheckBox'; import { TagContext } from '../context/TagContext'; +import usePost from '../apiHooks/usePost'; +import AuthorityRadioContainer from '../components/AuthorityRadioContainer'; type NewTopicFormValuesType = Omit; const NewTopic = () => { - const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 - const [isAll, setIsAll] = useState(true); // 모두 : 지정 인원 - const { openModal, closeModal } = useContext(ModalContext); + const { routePage } = useNavigator(); + const { state: pulledPinIds } = useLocation(); + const { showToast } = useToast(); + const { width } = useSetLayoutWidth(SIDEBAR); + const { navbarHighlights: _ } = useSetNavbarHighlight('addMapOrPin'); + const { setTags } = useContext(TagContext); + const { fetchPost } = usePost(); const { formValues, errorMessages, onChangeInput } = useFormValues({ name: '', description: '', image: '', }); - const { routePage } = useNavigator(); - const { state: taggedIds } = useLocation(); - const { showToast } = useToast(); - const { width } = useSetLayoutWidth(SIDEBAR); - const { navbarHighlights: _ } = useSetNavbarHighlight('addMapOrPin'); - const { setTags } = useContext(TagContext); - - const [members, setMembers] = useState([]); - - useEffect(() => { - const getMemberData = async () => { - const memberData = await getApi(`/members`); - setMembers(memberData); - }; - getMemberData(); - }, []); - - //해당 토픽에 권한을 부여할 아이디들을 담는 state - //addAuthority에 인자로 넘겨줌 - const [checkedMemberIds, setCheckedMemberIds] = useState([]); - - const handleChecked = (isChecked: boolean, id: number) => - setCheckedMemberIds((prev: MemberProps['id'][]) => - isChecked ? [...prev, id] : prev.filter((n: number) => n !== id), - ); + const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 + const [isAll, setIsAll] = useState(true); // 모두 : 지정 인원 + const [authorizedMemberIds, setAuthorizedMemberIds] = useState([]); const goToBack = () => { routePage(-1); @@ -73,106 +50,54 @@ const NewTopic = () => { return; } - if (isPrivate) { - const topicId = await postToServer(); + const topicId = await createTopic(); - const result = await addAuthority(topicId); - if (topicId) routePage(`/topics/${topicId}`); - return; - } - - if (!isPrivate && !isAll) { - const topicId = await postToServer(); - - const result = await addAuthority(topicId); - if (topicId) routePage(`/topics/${topicId}`); - return; - } - - if (!isAll && checkedMemberIds.length === 0) { - showToast('error', '멤버를 선택해주세요.'); - return; - } - - //생성하기 버튼 눌렀을 때 postToServer로 TopicId 받고, 받은 topicId로 권한 추가 - try { - const topicId = await postToServer(); - - if (topicId) routePage(`/topics/${topicId}`); - - showToast('info', '지도를 생성하였습니다.'); - } catch { - showToast( - 'error', - '지도 생성에 실패하였습니다. 입력하신 항목들을 다시 확인해주세요.', - ); + if (topicId) { + await addAuthorityToTopicWithGroupPermission(topicId); + routePage(`/topics/${topicId}`); } }; - const postToServer = async () => { - const response = - taggedIds?.length > 1 && typeof taggedIds !== 'string' - ? await mergeTopics() - : await createTopic(); + const createTopic = async () => { + const response = await postToServer(); const location = response?.headers.get('Location'); if (location) { const topicIdFromLocation = location.split('/')[2]; - return topicIdFromLocation; + return Number(topicIdFromLocation); } }; - //header의 location으로 받아온 topicId에 권한 추가 기능 - const addAuthority = async (topicId: any) => { - if (isAll && !isPrivate) return; // 모두 권한 준거면 return - - const response = await postApi(`/permissions`, { - topicId: topicId, - memberIds: checkedMemberIds, - }); - return response; - }; - - const mergeTopics = async () => { - try { - return await postApi('/topics/merge', { + const postToServer = async () => { + return fetchPost({ + url: '/topics/new', + payload: { image: formValues.image || DEFAULT_TOPIC_IMAGE, name: formValues.name, description: formValues.description, - topics: taggedIds, + pins: pulledPinIds ? pulledPinIds.split(',') : [], publicity: isPrivate ? 'PRIVATE' : 'PUBLIC', - permissionType: isAll ? 'ALL_MEMBERS' : 'GROUP_ONLY', - }); - } catch { - showToast( - 'error', + permissionType: isAll && !isPrivate ? 'ALL_MEMBERS' : 'GROUP_ONLY', + }, + errorMessage: '지도 생성에 실패하였습니다. 입력하신 항목들을 다시 확인해주세요.', - ); - throw new Error('지도 생성 실패'); - } + onSuccess: () => { + showToast('info', `${formValues.name} 지도를 생성하였습니다.`); + }, + }); }; - const createTopic = async () => { - try { - return await postApi('/topics/new', { - image: formValues.image || DEFAULT_TOPIC_IMAGE, - name: formValues.name, - description: formValues.description, - pins: typeof taggedIds === 'string' ? taggedIds.split(',') : [], - publicity: isPrivate ? 'PRIVATE' : 'PUBLIC', - permissionType: isPrivate - ? 'GROUP_ONLY' - : isAll - ? 'ALL_MEMBERS' - : 'GROUP_ONLY', - }); - } catch { - showToast( - 'error', - '지도 생성에 실패하였습니다. 입력하신 항목들을 다시 확인해주세요.', - ); - throw new Error('지도 생성 실패'); - } + const addAuthorityToTopicWithGroupPermission = async (topicId: number) => { + if (isAll) return; + + fetchPost({ + url: '/permissions', + payload: { + topicId, + memberIds: isPrivate ? [] : authorizedMemberIds, + }, + errorMessage: `${formValues.name} 지도의 권한 설정에 실패했습니다.`, + }); }; return ( @@ -185,7 +110,9 @@ const NewTopic = () => { 지도 생성 + + { errorMessage={errorMessages.image} maxLength={2048} /> + + { errorMessage={errorMessages.name} maxLength={20} /> + + { errorMessage={errorMessages.description} maxLength={100} /> - - - 공개 여부 - - - -
- setIsPrivate(false)} - tabIndex={4} - /> - -
- -
- setIsPrivate(true)} - tabIndex={4} - /> - -
-
- - - - 핀 생성 및 수정 권한 - - -
- { - setIsAll(true); - }} - tabIndex={5} - /> - {isPrivate ? ( - - ) : ( - - )} -
- -
- { - setIsAll(false); - openModal('newTopic'); - setCheckedMemberIds([]); - }} - tabIndex={5} - /> - -
-
- <> - - - - - 멤버 선택 - - - {checkedMemberIds.length}명 선택됨 - - - - - {members.map((member) => ( - - - - ))} - - - - - - + - - - - - - + + {user.email} From a126c4b916c8a6edbd797ee24e3a95b19d516894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:38:23 +0900 Subject: [PATCH 33/53] =?UTF-8?q?[FE]=20Fix/permission=20=ED=86=A0?= =?UTF-8?q?=ED=94=BD=20=EA=B6=8C=ED=95=9C=20=EC=88=98=EC=A0=95=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 모아보기 시에도 토픽 수정이 가능토록 변경 * refactor: 에러메세지 수정 * fix: 이전 권한 설정을 불러오지 못하는 에러 수정 추가로 기존에 선택한 친구들을 조건적으로 UI 렌더링을 진행했던 로직을 수정한다. * refactor: 이전에 권한을 부여한 친구들 목록 로직 수정 * test: cypress 개발 서버에 의존인 관계로 임시로 임계값 확장 * refactor: 변수명, 함수명 변경 --- frontend/cypress.config.ts | 2 +- frontend/cypress/e2e/mapbefine.cy.ts | 4 +- .../AuthorityRadioContainer/index.tsx | 47 ++++++++++++------- .../components/TopicInfo/UpdatedTopicInfo.tsx | 18 ++++--- frontend/src/pages/NewTopic.tsx | 8 ++-- frontend/src/pages/SelectedTopic.tsx | 2 +- frontend/src/types/Topic.ts | 4 +- 7 files changed, 47 insertions(+), 38 deletions(-) diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 35938082..d7ec6ab2 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', - defaultCommandTimeout: 10000, + defaultCommandTimeout: 20000, }, }); diff --git a/frontend/cypress/e2e/mapbefine.cy.ts b/frontend/cypress/e2e/mapbefine.cy.ts index f9a84f34..cbb675fe 100644 --- a/frontend/cypress/e2e/mapbefine.cy.ts +++ b/frontend/cypress/e2e/mapbefine.cy.ts @@ -69,7 +69,7 @@ describe('토픽 상세 페이지', () => { if (index === 0) $el.click(); }); - cy.wait(3000); + cy.wait(5000); cy.get('span').each(($el, index) => { if (index === 6) $el.click(); @@ -87,7 +87,7 @@ describe('토픽 상세 페이지', () => { if (index === 0) $el.click(); }); - cy.wait(2000); + cy.wait(5000); cy.get('span').each(($el, index) => { if (index === 6) $el.click(); diff --git a/frontend/src/components/AuthorityRadioContainer/index.tsx b/frontend/src/components/AuthorityRadioContainer/index.tsx index f2bffd8c..eaea281e 100644 --- a/frontend/src/components/AuthorityRadioContainer/index.tsx +++ b/frontend/src/components/AuthorityRadioContainer/index.tsx @@ -3,7 +3,10 @@ import Text from '../common/Text'; import Space from '../common/Space'; import Flex from '../common/Flex'; import { useContext, useEffect, useState } from 'react'; -import { TopicAuthorMember, TopicAuthorMemberWithId } from '../../types/Topic'; +import { + TopicAuthorMember, + TopicAuthorMemberWithAuthorId, +} from '../../types/Topic'; import { ModalContext } from '../../context/ModalContext'; import Box from '../common/Box'; import Modal from '../Modal'; @@ -13,17 +16,17 @@ import useGet from '../../apiHooks/useGet'; interface AuthorityRadioContainer { isPrivate: boolean; - isAll: boolean; + isPublic: boolean; authorizedMemberIds: number[]; setIsPrivate: React.Dispatch>; setIsAll: React.Dispatch>; setAuthorizedMemberIds: React.Dispatch>; - permissionedMembers?: TopicAuthorMemberWithId[]; + permissionedMembers?: TopicAuthorMemberWithAuthorId[]; } const AuthorityRadioContainer = ({ isPrivate, - isAll, + isPublic, authorizedMemberIds, setIsPrivate, setIsAll, @@ -34,6 +37,8 @@ const AuthorityRadioContainer = ({ const { fetchGet } = useGet(); const [members, setMembers] = useState([]); + const viewPrevAuthorMembersCondition = + authorizedMemberIds.length === 0 && !isPublic; useEffect(() => { fetchGet( @@ -108,7 +113,7 @@ const AuthorityRadioContainer = ({ @@ -125,10 +130,10 @@ const AuthorityRadioContainer = ({ { - isAll === false && openModal('newTopic'); + isPublic === false && openModal('newTopic'); }} tabIndex={5} /> @@ -162,25 +167,31 @@ const AuthorityRadioContainer = ({ )} - {authorizedMemberIds.length === 0 && permissionedMembers && ( + {permissionedMembers && viewPrevAuthorMembersCondition && ( <> - 기존에 선택한 친구들 + 이전에 권한을 부여한 친구들 - {permissionedMembers.map((member) => ( - - • {member.memberResponse.nickName} + {permissionedMembers.length > 0 ? ( + permissionedMembers.map((member) => ( + + • {member.memberResponse.nickName} + + )) + ) : ( + + • 없음 - ))} + )} )} diff --git a/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx b/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx index b8641545..5ee59b25 100644 --- a/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx +++ b/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx @@ -47,7 +47,7 @@ const UpdatedTopicInfo = ({ const [topicAuthorInfo, setTopicAuthorInfo] = useState(null); const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 - const [isAll, setIsAll] = useState(true); // 모두 : 지정 인원 + const [isPublic, setIsPublic] = useState(true); // 모두 : 지정 인원 const [authorizedMemberIds, setAuthorizedMemberIds] = useState([]); const updateTopicInfo = async () => { @@ -59,9 +59,9 @@ const UpdatedTopicInfo = ({ image, description: formValues.description, publicity: isPrivate ? 'PRIVATE' : 'PUBLIC', - permissionType: isAll && !isPrivate ? 'ALL_MEMBERS' : 'GROUP_ONLY', + permissionType: isPublic && !isPrivate ? 'ALL_MEMBERS' : 'GROUP_ONLY', }, - errorMessage: `'공개 ➡️ 비공개', '모두 ➡️ 친구들', '친구들 ➡️ 혼자' 로 변경할 수 없습니다.`, + errorMessage: `권한은 '공개 ➡️ 비공개', '모두 ➡️ 친구', '친구 ➡️ 혼자' 로 변경할 수 없습니다.`, isThrow: true, }); @@ -75,7 +75,7 @@ const UpdatedTopicInfo = ({ const updateTopicAuthority = async () => { // topicAuthorInfo api 구조 이상으로 권한 설정 자체에 대한 id를 사용 (topicId 아님) await fetchDelete({ - url: `/permissions/${topicAuthorInfo?.permissionMembers[0].id}`, + url: `/permissions/${topicAuthorInfo?.permissionedMembers[0].id}`, errorMessage: '권한 삭제에 실패했습니다.', isThrow: true, }); @@ -103,9 +103,7 @@ const UpdatedTopicInfo = ({ setTopicAuthorInfo(response); setIsPrivate(response.publicity === 'PRIVATE'); - if (topicAuthorInfo) { - setIsAll(topicAuthorInfo?.permissionMembers.length === 0); - } + setIsPublic(response.permissionedMembers.length === 0); }, ); }, []); @@ -142,12 +140,12 @@ const UpdatedTopicInfo = ({ diff --git a/frontend/src/pages/NewTopic.tsx b/frontend/src/pages/NewTopic.tsx index 28c1f06e..551daf42 100644 --- a/frontend/src/pages/NewTopic.tsx +++ b/frontend/src/pages/NewTopic.tsx @@ -36,7 +36,7 @@ const NewTopic = () => { }); const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 - const [isAll, setIsAll] = useState(true); // 모두 : 지정 인원 + const [isPublic, setIsPublic] = useState(true); // 모두 : 지정 인원 const [authorizedMemberIds, setAuthorizedMemberIds] = useState([]); const [showImage, setShowImage] = useState(''); @@ -104,7 +104,7 @@ const NewTopic = () => { }; const addAuthorityToTopicWithGroupPermission = async (topicId: number) => { - if (isAll) return; + if (isPublic) return; fetchPost({ url: '/permissions', @@ -189,10 +189,10 @@ const NewTopic = () => { diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index dd3b0437..c39a7a2f 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -126,7 +126,7 @@ const SelectedTopic = () => { {topicDetails.map((topicDetail, idx) => ( Date: Wed, 20 Sep 2023 20:41:41 +0900 Subject: [PATCH 34/53] =?UTF-8?q?hotfix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20post=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD=20(#468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MyInfo/index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/MyInfo/index.tsx b/frontend/src/components/MyInfo/index.tsx index 463678c9..e1e331ec 100644 --- a/frontend/src/components/MyInfo/index.tsx +++ b/frontend/src/components/MyInfo/index.tsx @@ -8,7 +8,7 @@ import { ProfileProps } from '../../types/Profile'; import UpdateMyInfo from './UpdateMyInfo'; import Button from '../common/Button'; import useToast from '../../hooks/useToast'; -import { postApi } from '../../apis/postApi'; +import { DEFAULT_PROD_URL } from '../../constants'; const user = JSON.parse(localStorage.getItem('user') || '{}'); const accessToken = localStorage.getItem('userToken'); @@ -25,13 +25,16 @@ const MyInfo = () => { const onClickLogout = async (e: React.MouseEvent) => { try { - await postApi( - `/logout`, - { - accessToken: accessToken, + fetch(`${DEFAULT_PROD_URL}/logout`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, }, - 'x-www-form-urlencoded', - ); + body: JSON.stringify({ + accessToken: accessToken, + }), + }); localStorage.removeItem('user'); localStorage.removeItem('userToken'); From d2dabd48de4c9cd176aa4757874721dfdf1c8092 Mon Sep 17 00:00:00 2001 From: ParkGeunCheol <72205402+GC-Park@users.noreply.github.com> Date: Wed, 20 Sep 2023 21:01:17 +0900 Subject: [PATCH 35/53] =?UTF-8?q?[FE]=20Feature/#435=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=95=95=EC=B6=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 이미지 압축하는 기능 custom hook으로 구현 * feat: 지도, 핀 등에 이미지 추가할 시 이미지 압축 기능 추가 * refactor: NewTopic, NewPin 텍스트 수정 * refactor: 이미지 리사이징 최대 크기 변경 * refactor: 지도 추가 페이지에서 이미지와 파일 추가 버튼 사이에 space 추가 --- frontend/src/hooks/useCompressImage.ts | 30 ++++++++++++++++++++++++++ frontend/src/pages/NewPin.tsx | 29 +++++++++++++------------ frontend/src/pages/NewTopic.tsx | 17 ++++++++++++--- frontend/src/pages/PinDetail.tsx | 6 +++++- 4 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 frontend/src/hooks/useCompressImage.ts diff --git a/frontend/src/hooks/useCompressImage.ts b/frontend/src/hooks/useCompressImage.ts new file mode 100644 index 00000000..52579a0c --- /dev/null +++ b/frontend/src/hooks/useCompressImage.ts @@ -0,0 +1,30 @@ +import imageCompression from 'browser-image-compression'; + +const useCompressImage = () => { + const compressImage = async (file: File) => { + const resizingBlob = await imageCompression(file, { + maxSizeMB: 1, + maxWidthOrHeight: 750, + useWebWorker: true, + }); + const resizingFile = new File([resizingBlob], file.name, { + type: file.type, + }); + return resizingFile; + }; + + const compressImageList = async (files: FileList) => { + const compressedImageList: File[] = []; + + for (const file of files) { + const compressedImage = await compressImage(file); + compressedImageList.push(compressedImage); + } + + return compressedImageList; + }; + + return { compressImage, compressImageList }; +}; + +export default useCompressImage; diff --git a/frontend/src/pages/NewPin.tsx b/frontend/src/pages/NewPin.tsx index 1e1233f4..799b194c 100644 --- a/frontend/src/pages/NewPin.tsx +++ b/frontend/src/pages/NewPin.tsx @@ -24,6 +24,7 @@ import Modal from '../components/Modal'; import { styled } from 'styled-components'; import ModalMyTopicList from '../components/ModalMyTopicList'; import { getMapApi } from '../apis/getMapApi'; +import useCompressImage from '../hooks/useCompressImage'; type NewPinFormValueType = Pick< NewPinFormProps, @@ -49,6 +50,7 @@ const NewPin = () => { const { showToast } = useToast(); const { width } = useSetLayoutWidth(SIDEBAR); const { openModal, closeModal } = useContext(ModalContext); + const { compressImageList } = useCompressImage(); const [formImages, setFormImages] = useState([]); @@ -177,7 +179,9 @@ const NewPin = () => { }); }; - const onPinImageChange = (event: React.ChangeEvent) => { + const onPinImageChange = async ( + event: React.ChangeEvent, + ) => { const imageLists = event.target.files; let imageUrlLists = [...showedImages]; @@ -189,8 +193,10 @@ const NewPin = () => { return; } + const compressedImageList = await compressImageList(imageLists); + for (let i = 0; i < imageLists.length; i++) { - const currentImageUrl = URL.createObjectURL(imageLists[i]); + const currentImageUrl = URL.createObjectURL(compressedImageList[i]); imageUrlLists.push(currentImageUrl); } @@ -203,7 +209,7 @@ const NewPin = () => { return; } - setFormImages([...formImages, ...imageLists]); + setFormImages([...formImages, ...compressedImageList]); setShowedImages(imageUrlLists); }; @@ -239,16 +245,12 @@ const NewPin = () => { - - - 지도 선택 - - - - * - - - + + 지도 선택 + + + +
@@ -95,4 +102,11 @@ const MyInfoImg = styled.img` border-radius: 50%; `; +const SettingContainer = styled.div` + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; +`; + export default MyInfo; diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts index 16f579c8..6e9c0620 100644 --- a/frontend/src/types/Profile.ts +++ b/frontend/src/types/Profile.ts @@ -1,4 +1,5 @@ export interface ProfileProps { name: string; email: string; + image: string; } From 85cd54e78b99c7c9ab867f4fe13282222a5fd089 Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Wed, 27 Sep 2023 11:36:51 +0900 Subject: [PATCH 51/53] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=EC=8B=9C=20localstorage=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MyInfo/UpdateMyInfo.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/MyInfo/UpdateMyInfo.tsx b/frontend/src/components/MyInfo/UpdateMyInfo.tsx index 667b44b4..ab889e5e 100644 --- a/frontend/src/components/MyInfo/UpdateMyInfo.tsx +++ b/frontend/src/components/MyInfo/UpdateMyInfo.tsx @@ -34,6 +34,7 @@ const UpdateMyInfo = ({ errorMessage: '회원정보 수정에 실패했습니다.', isThrow: true, onSuccess: () => { + localStorage.setItem('user', JSON.stringify(myInfo)); setIsModifyMyInfo(false); }, }); From b06ed2465d635548fb0055e2259c940be5b98fda Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:39:00 +0900 Subject: [PATCH 52/53] =?UTF-8?q?refactor:=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=98=EC=98=81=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MyInfo/UpdateMyInfo.tsx | 6 +++--- frontend/src/components/MyInfo/index.tsx | 4 ++-- frontend/src/types/Profile.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/MyInfo/UpdateMyInfo.tsx b/frontend/src/components/MyInfo/UpdateMyInfo.tsx index ab889e5e..525d907b 100644 --- a/frontend/src/components/MyInfo/UpdateMyInfo.tsx +++ b/frontend/src/components/MyInfo/UpdateMyInfo.tsx @@ -22,14 +22,14 @@ const UpdateMyInfo = ({ const onChangeMyInfoName = (e: React.ChangeEvent) => { if (e.target.value.length >= 20) return; - setMyInfo({ ...myInfo, name: e.target.value }); + setMyInfo({ ...myInfo, nickName: e.target.value }); }; const onClickModifyButton = async () => { await fetchPatch({ url: '/members/my/profiles', payload: { - nickName: myInfo.name, + nickName: myInfo.nickName, }, errorMessage: '회원정보 수정에 실패했습니다.', isThrow: true, @@ -51,7 +51,7 @@ const UpdateMyInfo = ({ - + {myInfo.email} diff --git a/frontend/src/components/MyInfo/index.tsx b/frontend/src/components/MyInfo/index.tsx index 28576ce3..db15d04d 100644 --- a/frontend/src/components/MyInfo/index.tsx +++ b/frontend/src/components/MyInfo/index.tsx @@ -11,15 +11,15 @@ import useToast from '../../hooks/useToast'; import { DEFAULT_PROD_URL } from '../../constants'; import Setting from '../../assets/setting.svg'; -const user = JSON.parse(localStorage.getItem('user') || '{}'); const accessToken = localStorage.getItem('userToken'); const MyInfo = () => { const { showToast } = useToast(); + const user = JSON.parse(localStorage.getItem('user') || '{}'); const [isModifyMyInfo, setIsModifyMyInfo] = useState(false); const [myInfo, setMyInfo] = useState({ - name: user.nickName, + nickName: user.nickName, email: user.email, image: user.imageUrl, }); diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts index 6e9c0620..825f40ef 100644 --- a/frontend/src/types/Profile.ts +++ b/frontend/src/types/Profile.ts @@ -1,5 +1,5 @@ export interface ProfileProps { - name: string; + nickName: string; email: string; image: string; } From 5d5a93afc03bdf25f206a5212642d60791fbacb2 Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:05:12 +0900 Subject: [PATCH 53/53] =?UTF-8?q?feat:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/fe-merge-dev.yml | 8 +++++--- .github/workflows/fe-merge-prod.yml | 8 +++++--- frontend/src/pages/Home.tsx | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/fe-merge-dev.yml b/.github/workflows/fe-merge-dev.yml index b9e6bf40..65a21c08 100644 --- a/.github/workflows/fe-merge-dev.yml +++ b/.github/workflows/fe-merge-dev.yml @@ -23,14 +23,16 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 - cache: 'npm' - cache-dependency-path: 'frontend' + cache: "npm" + cache-dependency-path: "frontend" - name: Install npm run: npm install working-directory: frontend - name: Build project + env: + APP_URL: "https://mapbefine.kro.kr/api" run: npm run build working-directory: frontend @@ -63,7 +65,7 @@ jobs: uses: 8398a7/action-slack@v3 with: - mention: 'here' + mention: "here" if_mention: always status: ${{ job.status }} fields: workflow,job,commit,message,ref,author,took diff --git a/.github/workflows/fe-merge-prod.yml b/.github/workflows/fe-merge-prod.yml index 7aec838a..cc30ac13 100644 --- a/.github/workflows/fe-merge-prod.yml +++ b/.github/workflows/fe-merge-prod.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: - branches: [ main ] + branches: [main] types: [closed] paths: frontend/** @@ -31,6 +31,8 @@ jobs: working-directory: frontend - name: Build project + env: + APP_URL: "https://mapbefine.com/api" run: npm run build working-directory: frontend @@ -41,7 +43,7 @@ jobs: path: frontend/dist deploy: - runs-on: [ self-hosted, prod] + runs-on: [self-hosted, prod] needs: build-and-upload if: github.event.pull_request.merged @@ -63,7 +65,7 @@ jobs: uses: 8398a7/action-slack@v3 with: - mention: 'here' + mention: "here" if_mention: always status: ${{ job.status }} fields: workflow,job,commit,message,ref,author,took diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 5ca139b9..b14bbd26 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -14,6 +14,7 @@ const TopicListContainer = lazy( ); const Home = () => { + console.log(`${process.env.APP_URL}`, '환경변수 테스트'); const { routingHandlers } = useNavigator(); const { goToPopularTopics, goToLatestTopics, goToNearByMeTopics } = routingHandlers;