diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index 2c213fc0..64e532a2 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -18,6 +18,7 @@ "^@type/(.*)$", "^@apis/(.*)$", "^@mocks/(.*)$", + "^@queries/(.*)$", "^@components/(.*)$", "^@hooks/(.*)$", "^@constants/(.*)$", diff --git a/frontend/__tests__/mainPage.test.tsx b/frontend/__tests__/mainPage.test.tsx new file mode 100644 index 00000000..936d0c8f --- /dev/null +++ b/frontend/__tests__/mainPage.test.tsx @@ -0,0 +1,45 @@ +import { waitFor } from "@testing-library/react"; + +import { createInfiniteTravelogueHook } from "./utils/createInfiniteTravelogueHook"; + +describe("메인 페이지", () => { + describe("여행기 무한 스크롤 확인", () => { + it("메인 페이지로 진입했을때 보여지는 여행기는 총 5개이다.", async () => { + const { result } = createInfiniteTravelogueHook(); + await waitFor(() => { + expect(result.current.status).toMatch("success"); + expect(result.current.travelogues.length).toBe(5); + }); + }); + + it("다음 페이지로 이동할 경우 보여지는 여행기는 총 10개이다.", async () => { + const { result } = createInfiniteTravelogueHook(); + + await waitFor(() => { + result.current.fetchNextPage(); + }); + + await waitFor(() => { + expect(result.current.status).toMatch("success"); + expect(result.current.travelogues.length).toBe(10); + }); + }); + + it("마지막 페이지로 이동할 경우 더 이상 콘텐츠를 확인할 수 없다.", async () => { + const { result } = createInfiniteTravelogueHook(); + + await waitFor(() => { + result.current.fetchNextPage(); + }); + + await waitFor(() => { + result.current.fetchNextPage(); + }); + + await waitFor(() => { + expect(result.current.status).toMatch("success"); + expect(result.current.hasNextPage).toBeFalsy(); + }); + }); + }); +}); diff --git a/frontend/__tests__/utils/createInfiniteTravelogueHook.tsx b/frontend/__tests__/utils/createInfiniteTravelogueHook.tsx new file mode 100644 index 00000000..8f26370f --- /dev/null +++ b/frontend/__tests__/utils/createInfiniteTravelogueHook.tsx @@ -0,0 +1,22 @@ +import { renderHook } from "@testing-library/react"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import useInfiniteTravelogues from "@queries/useInfiniteTravelogues"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 0, + }, + }, +}); + +export const wrapper = ({ children }: React.PropsWithChildren) => ( + {children} +); + +export const createInfiniteTravelogueHook = () => + renderHook(() => useInfiniteTravelogues(), { + wrapper, + }); diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 3c57e3f1..92a0b3ff 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -22,6 +22,8 @@ module.exports = { (https://mswjs.io/docs/migrations/1.x-to-2.x/#frequent-issues) */ setupFiles: ["./jest.polyfills.js"], + setupFilesAfterEnv: ["./jest-setup.ts"], + testEnvironmentOptions: { customExportConditions: [""], }, diff --git a/frontend/src/apis/ApiError.ts b/frontend/src/apis/ApiError.ts index 897f2c20..c5190655 100644 --- a/frontend/src/apis/ApiError.ts +++ b/frontend/src/apis/ApiError.ts @@ -1,12 +1,6 @@ import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios"; -const HTTP_STATUS = { - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500, -} as const; +import { HTTP_STATUS_CODE_MAP } from "@constants/httpStatusCode"; class ApiError extends Error implements AxiosError { config: InternalAxiosRequestConfig; @@ -23,23 +17,23 @@ class ApiError extends Error implements AxiosError { const errorStatus = error.response?.status || 0; let name = "ApiError"; - if (errorStatus === HTTP_STATUS.BAD_REQUEST) { + if (errorStatus === HTTP_STATUS_CODE_MAP.BAD_REQUEST) { name = "ApiBadRequestError"; } - if (errorStatus === HTTP_STATUS.UNAUTHORIZED) { + if (errorStatus === HTTP_STATUS_CODE_MAP.UNAUTHORIZED) { name = "ApiUnauthorizedError"; } - if (errorStatus === HTTP_STATUS.FORBIDDEN) { + if (errorStatus === HTTP_STATUS_CODE_MAP.FORBIDDEN) { name = "ApiForbiddenError"; } - if (errorStatus === HTTP_STATUS.NOT_FOUND) { + if (errorStatus === HTTP_STATUS_CODE_MAP.NOT_FOUND) { name = "ApiNotFoundError"; } - if (errorStatus === HTTP_STATUS.INTERNAL_SERVER_ERROR) { + if (errorStatus === HTTP_STATUS_CODE_MAP.INTERNAL_SERVER_ERROR) { name = "ApiInternalServerError"; } diff --git a/frontend/src/apis/interceptor.ts b/frontend/src/apis/interceptor.ts index c7833766..de8c4a77 100644 --- a/frontend/src/apis/interceptor.ts +++ b/frontend/src/apis/interceptor.ts @@ -2,19 +2,21 @@ import * as Sentry from "@sentry/react"; import { AxiosError, InternalAxiosRequestConfig } from "axios"; import type { ErrorResponse } from "@type/api/errorResponse"; -import type { User } from "@type/domain/user"; +import type { UserResponse } from "@type/domain/user"; import ApiError from "@apis/ApiError"; -import { ROUTE_PATHS } from "@constants/route"; +import { ERROR_MESSAGE_MAP } from "@constants/errorMessage"; +import { ROUTE_PATHS_MAP } from "@constants/route"; +import { STORAGE_KEYS_MAP } from "@constants/storage"; export const checkAccessToken = ( config: InternalAxiosRequestConfig, accessToken: string | null, ) => { if (!accessToken) { - alert("로그인이 필요합니다."); - window.location.href = ROUTE_PATHS.login; + alert(ERROR_MESSAGE_MAP.api.login); + window.location.href = ROUTE_PATHS_MAP.login; } return config; @@ -32,7 +34,7 @@ export const setAuthorizationHeader = ( }; export const handlePreviousRequest = (config: InternalAxiosRequestConfig) => { - const user: User | null = JSON.parse(localStorage.getItem("tourootUser") ?? "{}"); + const user: UserResponse | null = JSON.parse(localStorage.getItem(STORAGE_KEYS_MAP.user) ?? "{}"); let newConfig = { ...config }; newConfig = checkAccessToken(config, user?.accessToken ?? null); diff --git a/frontend/src/assets/svg/index.ts b/frontend/src/assets/svg/index.ts index da4924e3..5863139e 100644 --- a/frontend/src/assets/svg/index.ts +++ b/frontend/src/assets/svg/index.ts @@ -19,3 +19,4 @@ export { default as ImageUpload } from "./image-upload.svg"; export { default as KaKao } from "./kakao.svg"; export { default as KoreanLogo } from "./korean-logo.svg"; export { default as Plus } from "./plus.svg"; +export { default as tturiUrl } from "./tturi.svg?url"; diff --git a/frontend/src/components/common/Accordion/AccordionItem/accordionItemContext.ts b/frontend/src/components/common/Accordion/AccordionItem/accordionItemContext.ts index 42378d21..8900e84d 100644 --- a/frontend/src/components/common/Accordion/AccordionItem/accordionItemContext.ts +++ b/frontend/src/components/common/Accordion/AccordionItem/accordionItemContext.ts @@ -1,11 +1,13 @@ import { createContext, useContext } from "react"; +import { ERROR_MESSAGE_MAP } from "@constants/errorMessage"; + export const AccordionItemContext = createContext(""); export const useAccordionItemContext = () => { const context = useContext(AccordionItemContext); if (!context) { - throw new Error("Provider 바깥에 존재합니다!"); + throw new Error(ERROR_MESSAGE_MAP.provider); } return context; diff --git a/frontend/src/components/common/Accordion/AccordionRoot/accordionContext.ts b/frontend/src/components/common/Accordion/AccordionRoot/accordionContext.ts index 1fc95d68..ae48ce28 100644 --- a/frontend/src/components/common/Accordion/AccordionRoot/accordionContext.ts +++ b/frontend/src/components/common/Accordion/AccordionRoot/accordionContext.ts @@ -1,5 +1,7 @@ import { createContext, useContext } from "react"; +import { ERROR_MESSAGE_MAP } from "@constants/errorMessage"; + interface AccordionContextConfig { value: Set; handleToggleAccordion: (item: string) => void; @@ -10,7 +12,7 @@ export const AccordionContext = createContext(nul export const useAccordionContext = () => { const context = useContext(AccordionContext); if (!context) { - throw new Error("Provider 바깥에 존재합니다!"); + throw new Error(ERROR_MESSAGE_MAP.provider); } return context; diff --git a/frontend/src/components/common/Drawer/Drawer.tsx b/frontend/src/components/common/Drawer/Drawer.tsx index 49bfb676..9ad0bc5c 100644 --- a/frontend/src/components/common/Drawer/Drawer.tsx +++ b/frontend/src/components/common/Drawer/Drawer.tsx @@ -1,5 +1,7 @@ import React, { createContext, useContext, useState } from "react"; +import { ERROR_MESSAGE_MAP } from "@constants/errorMessage"; + import * as S from "./Drawer.styled"; interface DrawerContextType { @@ -14,7 +16,7 @@ const DrawerContext = createContext(undefined); const useDrawerContext = () => { const context = useContext(DrawerContext); if (!context) { - throw new Error("Drawer Provider가 없습니다."); + throw new Error(ERROR_MESSAGE_MAP.provider); } return context; }; diff --git a/frontend/src/components/common/FloatingButton/FloatingButton.tsx b/frontend/src/components/common/FloatingButton/FloatingButton.tsx index 7174a4c0..0f5b4a79 100644 --- a/frontend/src/components/common/FloatingButton/FloatingButton.tsx +++ b/frontend/src/components/common/FloatingButton/FloatingButton.tsx @@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom"; // import { UserContext } from "@contexts/UserProvider"; import Icon from "@components/common/Icon/Icon"; -import { ROUTE_PATHS } from "@constants/route"; +import { ROUTE_PATHS_MAP } from "@constants/route"; import { PRIMITIVE_COLORS } from "@styles/tokens"; @@ -26,7 +26,7 @@ const FloatingButton = () => { // alert("여행기 작성 전 로그인을 먼저 해주세요!"); // return; // } - navigate(ROUTE_PATHS.travelogueRegister); + navigate(ROUTE_PATHS_MAP.travelogueRegister); }; const handleClickShareButton = () => {}; diff --git a/frontend/src/components/common/GoogleSearchPopup/GoogleSearchPopup.tsx b/frontend/src/components/common/GoogleSearchPopup/GoogleSearchPopup.tsx index 818d28b1..2c6c43d6 100644 --- a/frontend/src/components/common/GoogleSearchPopup/GoogleSearchPopup.tsx +++ b/frontend/src/components/common/GoogleSearchPopup/GoogleSearchPopup.tsx @@ -4,7 +4,7 @@ import { Global, css } from "@emotion/react"; import { Autocomplete } from "@react-google-maps/api"; -import type { Place } from "@type/domain/travelogue"; +import type { TravelTransformPlace } from "@type/domain/travelTransform"; import { Button } from "@components/common"; @@ -12,7 +12,7 @@ import * as S from "./GoogleSearchPopup.styled"; interface GoogleSearchPopupProps { onClosePopup: () => void; - onSearchPlaceInfo: (placeInfo: Pick) => void; + onSearchPlaceInfo: (placeInfo: Pick) => void; } const GoogleSearchPopup = ({ onClosePopup, onSearchPlaceInfo }: GoogleSearchPopupProps) => { @@ -35,7 +35,7 @@ const GoogleSearchPopup = ({ onClosePopup, onSearchPlaceInfo }: GoogleSearchPopu lng: place.geometry.location.lng(), }; - const placeInfo: Pick = { + const placeInfo: Pick = { placeName: place.name || "", position: newCenter, }; diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx index 06e729d3..b1e1b69a 100644 --- a/frontend/src/components/common/Header/Header.tsx +++ b/frontend/src/components/common/Header/Header.tsx @@ -4,7 +4,7 @@ import IconButton from "@components/common/IconButton/IconButton"; import useUser from "@hooks/useUser"; -import { ROUTE_PATHS } from "@constants/route"; +import { ROUTE_PATHS_MAP } from "@constants/route"; import theme from "@styles/theme"; import { PRIMITIVE_COLORS } from "@styles/tokens"; @@ -21,9 +21,9 @@ const Header = () => { const navigate = useNavigate(); const handleClickButton = - pathName === ROUTE_PATHS.root || pathName === ROUTE_PATHS.login - ? () => navigate(ROUTE_PATHS.root) - : () => navigate(-1); + pathName === ROUTE_PATHS_MAP.root || pathName === ROUTE_PATHS_MAP.login + ? () => navigate(ROUTE_PATHS_MAP.root) + : () => navigate(ROUTE_PATHS_MAP.back); const { user } = useUser(); @@ -31,11 +31,11 @@ const Header = () => { - {pathName === ROUTE_PATHS.login ? ( + {pathName === ROUTE_PATHS_MAP.login ? ( <> 로그인 @@ -67,7 +67,7 @@ const Header = () => { ) : ( { - navigate(ROUTE_PATHS.login); + navigate(ROUTE_PATHS_MAP.login); }} > 로그인 diff --git a/frontend/src/components/common/Icon/svg-icons.json b/frontend/src/components/common/Icon/svg-icons.json index d63c338d..ad8e0193 100644 --- a/frontend/src/components/common/Icon/svg-icons.json +++ b/frontend/src/components/common/Icon/svg-icons.json @@ -201,9 +201,9 @@ "width": 11, "height": 11, "path": "M1.1002 11L0 9.9L4.40079 5.5L0 1.1L1.1002 0L5.50098 4.4L9.90177 0L11.002 1.1L6.60118 5.5L11.002 9.9L9.90177 11L5.50098 6.6L1.1002 11Z", - "stroke": "currentColor", - "strokeWidth": "1", - "strokeLinecap": "butt", - "strokeLinejoin": "miter" + "stroke": "", + "strokeWidth": "0", + "strokeLinecap": "", + "strokeLinejoin": "" } -} \ No newline at end of file +} diff --git a/frontend/src/components/common/Input/Input.styled.ts b/frontend/src/components/common/Input/Input.styled.ts index 69afb50b..900c10f3 100644 --- a/frontend/src/components/common/Input/Input.styled.ts +++ b/frontend/src/components/common/Input/Input.styled.ts @@ -29,6 +29,12 @@ export const Input = styled.input` outline: none; } + &:disabled { + background-color: ${({ theme }) => theme.colors.background.disabled}; + + color: ${({ theme }) => theme.colors.text.secondary}; + } + &::placeholder { color: ${({ theme }) => theme.colors.text.secondary}; } diff --git a/frontend/src/components/common/Input/Input.tsx b/frontend/src/components/common/Input/Input.tsx index 6cfc7ce0..9ce9866c 100644 --- a/frontend/src/components/common/Input/Input.tsx +++ b/frontend/src/components/common/Input/Input.tsx @@ -6,7 +6,7 @@ import * as S from "./Input.styled"; interface InputProps extends InputHTMLAttributes { count?: number; maxCount?: number; - label: string; + label?: string; } const Input = ({ label, count, maxCount, ...props }: InputProps) => { diff --git a/frontend/src/components/common/Modal/Modal.stories.tsx b/frontend/src/components/common/Modal/Modal.stories.tsx new file mode 100644 index 00000000..61218ad4 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.stories.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; + +import type { Meta } from "@storybook/react"; +import { fn } from "@storybook/test"; + +import Button from "@components/common/Button/Button"; +import Input from "@components/common/Input/Input"; +import Text from "@components/common/Text/Text"; + +import theme from "@styles/theme"; + +import Modal from "./Modal"; + +const meta = { + title: "common/Modal", + component: Modal, + parameters: { + layout: "fullscreen", + viewport: { + defaultViewport: "desktop", + }, + controls: { disable: true }, + }, + args: { onCloseModal: fn() }, +} satisfies Meta; + +export default meta; + +export const ShareModal = { + render: () => { + const [isOpen, setIsOpen] = useState(false); + const onToggleModal = () => setIsOpen((prev) => !prev); + + return ( + <> + + {isOpen && ( + + + + + + + 여행기를 공유할까요? + +
+ + +
+
+
+ )} + + ); + }, +}; + +export const TravelPlanDeleteModal = { + render: () => { + const [isOpen, setIsOpen] = useState(false); + const onToggleModal = () => setIsOpen((prev) => !prev); + + return ( + <> + + {isOpen && ( + + + + + +
+ + 여행 계획을 삭제할까요? + + + 삭제한 후에는 여행 계획을 다시 복구할 수 없어요. + +
+
+ + + +
+ )} + + ); + }, +}; diff --git a/frontend/src/components/common/Modal/Modal.style.ts b/frontend/src/components/common/Modal/Modal.style.ts new file mode 100644 index 00000000..37e4dd86 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.style.ts @@ -0,0 +1,36 @@ +import styled from "@emotion/styled"; + +import { PRIMITIVE_COLORS } from "@styles/tokens"; + +export const Layout = styled.section` + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset: 0; + z-index: 1000; +`; + +export const BackdropLayout = styled.div` + position: absolute; + width: 100%; + height: 100%; + + background-color: ${({ theme }) => theme.colors.dimmed}; + cursor: pointer; +`; + +export const ModalBoxLayout = styled.div` + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; + width: calc(100% - ${({ theme }) => theme.spacing.m} * 2); + max-height: 80vh; + max-width: calc(48rem - ${({ theme }) => theme.spacing.m} * 2); + margin: ${({ theme }) => theme.spacing.m}; + border-radius: ${({ theme }) => theme.spacing.s}; + + background-color: ${PRIMITIVE_COLORS.white}; + box-shadow: 0 0 5px rgb(0 0 0 / 15%); +`; diff --git a/frontend/src/components/common/Modal/Modal.tsx b/frontend/src/components/common/Modal/Modal.tsx new file mode 100644 index 00000000..a912147a --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +import ModalProvider from "@contexts/ModalProvider"; + +import ModalBody from "@components/common/Modal/ModalBody/ModalBody"; +import ModalFooter from "@components/common/Modal/ModalFooter/ModalFooter"; +import ModalHeader from "@components/common/Modal/ModalHeader/ModalHeader"; + +import useModalControl from "@hooks/useModalControl"; + +import * as S from "./Modal.style"; + +interface ModalProps extends React.PropsWithChildren { + onCloseModal: () => void; + isOpen: boolean; +} + +const Modal = ({ children, onCloseModal, isOpen }: ModalProps) => { + useModalControl(isOpen, onCloseModal); + + return ReactDOM.createPortal( + + + + {children} + + , + document.querySelector("#root") as HTMLElement, + ); +}; + +export default Modal; + +Modal.Header = ModalHeader; +Modal.Body = ModalBody; +Modal.Footer = ModalFooter; diff --git a/frontend/src/components/common/Modal/ModalBody/ModalBody.styled.ts b/frontend/src/components/common/Modal/ModalBody/ModalBody.styled.ts new file mode 100644 index 00000000..6bcde86a --- /dev/null +++ b/frontend/src/components/common/Modal/ModalBody/ModalBody.styled.ts @@ -0,0 +1,23 @@ +import styled from "@emotion/styled"; + +export const Layout = styled.div<{ $direction: React.CSSProperties["flexDirection"] }>` + display: flex; + flex: 1; + flex-direction: ${({ $direction }) => $direction}; + justify-content: center; + align-items: center; + width: 100%; + overflow-y: auto; + scrollbar-width: thin; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + border-radius: ${({ theme }) => theme.spacing.s}; + + background-color: ${({ theme }) => theme.colors.border}; + } +`; diff --git a/frontend/src/components/common/Modal/ModalBody/ModalBody.tsx b/frontend/src/components/common/Modal/ModalBody/ModalBody.tsx new file mode 100644 index 00000000..ca320f80 --- /dev/null +++ b/frontend/src/components/common/Modal/ModalBody/ModalBody.tsx @@ -0,0 +1,17 @@ +import { HTMLAttributes } from "react"; + +import * as S from "./ModalBody.styled"; + +interface ModalBodyProps extends React.PropsWithChildren, HTMLAttributes { + direction?: React.CSSProperties["flexDirection"]; +} + +const ModalBody = ({ children, direction = "row", ...props }: ModalBodyProps) => { + return ( + + {children} + + ); +}; + +export default ModalBody; diff --git a/frontend/src/components/common/Modal/ModalFooter/ModalFooter.styled.ts b/frontend/src/components/common/Modal/ModalFooter/ModalFooter.styled.ts new file mode 100644 index 00000000..b53999a2 --- /dev/null +++ b/frontend/src/components/common/Modal/ModalFooter/ModalFooter.styled.ts @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; + +export const Layout = styled.div<{ $direction: React.CSSProperties["flexDirection"] }>` + display: flex; + flex-direction: ${({ $direction }) => $direction}; + width: 100%; + padding: ${({ theme }) => theme.spacing.m}; +`; diff --git a/frontend/src/components/common/Modal/ModalFooter/ModalFooter.tsx b/frontend/src/components/common/Modal/ModalFooter/ModalFooter.tsx new file mode 100644 index 00000000..07e02d67 --- /dev/null +++ b/frontend/src/components/common/Modal/ModalFooter/ModalFooter.tsx @@ -0,0 +1,11 @@ +import * as S from "./ModalFooter.styled"; + +export interface ModalFooterProps extends React.PropsWithChildren { + direction?: React.CSSProperties["flexDirection"]; +} + +const ModalFooter = ({ children, direction = "row" }: ModalFooterProps) => { + return {children}; +}; + +export default ModalFooter; diff --git a/frontend/src/components/common/Modal/ModalHeader/ModalHeader.styled.ts b/frontend/src/components/common/Modal/ModalHeader/ModalHeader.styled.ts new file mode 100644 index 00000000..cf3f6ec9 --- /dev/null +++ b/frontend/src/components/common/Modal/ModalHeader/ModalHeader.styled.ts @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; + +export const Layout = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.6rem; +`; + +export const TitleWrapper = styled.div` + flex-grow: 1; + + text-align: center; +`; diff --git a/frontend/src/components/common/Modal/ModalHeader/ModalHeader.tsx b/frontend/src/components/common/Modal/ModalHeader/ModalHeader.tsx new file mode 100644 index 00000000..03569599 --- /dev/null +++ b/frontend/src/components/common/Modal/ModalHeader/ModalHeader.tsx @@ -0,0 +1,31 @@ +import { useModalContext } from "@contexts/ModalProvider"; + +import IconButton from "@components/common/IconButton/IconButton"; + +import * as S from "./ModalHeader.styled"; + +interface ModalHeaderProps extends React.PropsWithChildren { + hasCloseIcon?: boolean; + buttonPosition?: "left" | "right"; +} + +const ModalHeader = ({ + children, + hasCloseIcon = true, + buttonPosition = "right", +}: ModalHeaderProps) => { + const onCloseModal = useModalContext(); + return ( + + {buttonPosition === "left" && hasCloseIcon && ( + + )} + {children} + {buttonPosition === "right" && hasCloseIcon && ( + + )} + + ); +}; + +export default ModalHeader; diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index 90abbf8a..ceef520e 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -18,6 +18,5 @@ export { default as GoogleMapLoadScript } from "./GoogleMapLoadScript/GoogleMapL export { default as Text } from "./Text/Text"; export { default as Textarea } from "./Textarea/Textarea"; export { default as ThumbnailUpload } from "./ThumbnailUpload/ThumbnailUpload"; -export { default as DayContent } from "./DayContent/DayContent"; export { default as FallbackImage } from "./FallbackImage/FallbackImage"; export { default as AvatarCircle } from "./AvatarCircle/AvatarCircle"; diff --git a/frontend/src/components/pages/login/KakaoCallbackPage.tsx b/frontend/src/components/pages/login/KakaoCallbackPage.tsx index 64979fa4..b9e3d698 100644 --- a/frontend/src/components/pages/login/KakaoCallbackPage.tsx +++ b/frontend/src/components/pages/login/KakaoCallbackPage.tsx @@ -5,7 +5,9 @@ import { SaveUserContext } from "@contexts/UserProvider"; import { client } from "@apis/client"; -import { ROUTE_PATHS } from "@constants/route"; +import { API_ENDPOINT_MAP } from "@constants/endpoint"; +import { ERROR_MESSAGE_MAP } from "@constants/errorMessage"; +import { ROUTE_PATHS_MAP } from "@constants/route"; const KakaoCallbackPage = () => { const navigate = useNavigate(); @@ -22,15 +24,15 @@ const KakaoCallbackPage = () => { .post(`${ROUTE_PATHS.loginOauth}?code=${code}&redirectUri=${encodedRedirectUri}`) .then((res) => { saveUser(res.data); - navigate(ROUTE_PATHS.root); + navigate(ROUTE_PATHS_MAP.root); }) .catch(() => { - alert("로그인에 실패하였습니다. 다시 시도해주세요!"); - navigate(ROUTE_PATHS.login); + alert(ERROR_MESSAGE_MAP.loginFailed); + navigate(ROUTE_PATHS_MAP.login); }); } else { - alert("로그인에 실패하였습니다. 다시 시도해주세요!"); - navigate(ROUTE_PATHS.login); + alert(ERROR_MESSAGE_MAP.loginFailed); + navigate(ROUTE_PATHS_MAP.login); } }, [navigate]); diff --git a/frontend/src/components/pages/main/TravelogueCard/TravelogueCard.tsx b/frontend/src/components/pages/main/TravelogueCard/TravelogueCard.tsx index d591f4f2..6629f090 100644 --- a/frontend/src/components/pages/main/TravelogueCard/TravelogueCard.tsx +++ b/frontend/src/components/pages/main/TravelogueCard/TravelogueCard.tsx @@ -6,6 +6,8 @@ import { AvatarCircle, FallbackImage } from "@components/common"; import useImageError from "@hooks/useImageError"; +import { ROUTE_PATHS_MAP } from "@constants/route"; + import { EmptyHeart } from "@assets/svg"; import * as S from "./TravelogueCard.styled"; @@ -21,7 +23,7 @@ const TravelogueCard = ({ const { imageError, handleImageError } = useImageError({ imageUrl: thumbnail }); const handleCardClick = () => { - navigate(`/travelogue/${id}`); + navigate(ROUTE_PATHS_MAP.travelogue(id)); }; const handleLikeClick = (e: React.MouseEvent) => { diff --git a/frontend/src/components/pages/travelPlanDetail/TravelPlansTabContent/TravelPlansTabContent.tsx b/frontend/src/components/pages/travelPlanDetail/TravelPlansTabContent/TravelPlansTabContent.tsx index 2a0e1271..3efd5b42 100644 --- a/frontend/src/components/pages/travelPlanDetail/TravelPlansTabContent/TravelPlansTabContent.tsx +++ b/frontend/src/components/pages/travelPlanDetail/TravelPlansTabContent/TravelPlansTabContent.tsx @@ -1,10 +1,10 @@ -import type { Place } from "@type/domain/travelogue"; +import type { TravelPlanPlace } from "@type/domain/travelPlan"; import { Box, GoogleMapLoadScript, GoogleMapView } from "@components/common"; import * as S from "./TravelPlansTabContent.styled"; -const TravelPlansTabContent = ({ places }: { places: Place[] }) => { +const TravelPlansTabContent = ({ places }: { places: TravelPlanPlace[] }) => { if (places.length === 0) return null; const positions = places.map((place) => { diff --git a/frontend/src/components/common/DayContent/DayContent.tsx b/frontend/src/components/pages/travelPlanRegister/TravelPlanDayAccordion/TravelPlanDayAccordion.tsx similarity index 70% rename from frontend/src/components/common/DayContent/DayContent.tsx rename to frontend/src/components/pages/travelPlanRegister/TravelPlanDayAccordion/TravelPlanDayAccordion.tsx index ae487f91..b2787f48 100644 --- a/frontend/src/components/common/DayContent/DayContent.tsx +++ b/frontend/src/components/pages/travelPlanRegister/TravelPlanDayAccordion/TravelPlanDayAccordion.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import type { TravelRegisterDay, TravelRegisterPlace } from "@type/domain/travelogue"; +import { TravelPlanDay, TravelPlanPlace } from "@type/domain/travelPlan"; import { Accordion, @@ -10,19 +10,10 @@ import { Textarea, } from "@components/common"; -import * as S from "../../pages/travelogueRegister/TravelogueRegisterPage.styled"; +import * as S from "../TravelPlanRegisterPage.styled"; -const DayContent = ({ - children, - travelDay, - dayIndex, - onDeleteDay, - onDeletePlace, - onChangePlaceDescription, - onAddPlace, -}: { - children?: (placeIndex: number, previewUrls: { url: string }[]) => JSX.Element; - travelDay: TravelRegisterDay; +interface TravelPlanDayAccordionProps { + travelPlanDay: TravelPlanDay; dayIndex: number; onDeleteDay: (dayIndex: number) => void; onDeletePlace: (dayIndex: number, placeIndex: number) => void; @@ -31,12 +22,21 @@ const DayContent = ({ dayIndex: number, placeIndex: number, ) => void; - onAddPlace: (dayIndex: number, travelParams: TravelRegisterPlace) => void; -}) => { + onAddPlace: (dayIndex: number, travelParams: TravelPlanPlace) => void; +} + +const TravelPlanDayAccordion = ({ + travelPlanDay, + dayIndex, + onDeleteDay, + onDeletePlace, + onChangePlaceDescription, + onAddPlace, +}: TravelPlanDayAccordionProps) => { const [isPopupOpen, setIsPopupOpen] = useState(false); const onSelectSearchResult = ( - placeInfo: Pick, + placeInfo: Pick, dayIndex: number, ) => { onAddPlace(dayIndex, placeInfo); @@ -48,30 +48,29 @@ const DayContent = ({ }; return ( - + onDeleteDay(dayIndex)}> {`Day ${dayIndex + 1}`} ({ + places={travelPlanDay.places.map((place) => ({ lat: Number(place.position.lat), lng: Number(place.position.lng), }))} /> - {travelDay.places.map((place, placeIndex) => ( - + {travelPlanDay.places.map((place, placeIndex) => ( + onDeletePlace(dayIndex, placeIndex)}> {place.placeName || `장소 ${placeIndex + 1}`} - {children && children(placeIndex, place?.photoUrls ?? [])}