diff --git a/package-lock.json b/package-lock.json index 6dfdd286..2ed647cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-helmet-async": "^2.0.4", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", + "react-intersection-observer": "^9.5.3", "react-kakao-maps-sdk": "^1.1.24", "react-mobile-datepicker": "^4.0.2", "react-router-dom": "^6.21.1", @@ -10984,6 +10985,14 @@ "react": "*" } }, + "node_modules/react-intersection-observer": { + "version": "9.5.3", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.3.tgz", + "integrity": "sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 6e28e42a..3f46b91c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-helmet-async": "^2.0.4", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", + "react-intersection-observer": "^9.5.3", "react-kakao-maps-sdk": "^1.1.24", "react-mobile-datepicker": "^4.0.2", "react-router-dom": "^6.21.1", diff --git a/src/App.tsx b/src/App.tsx index ebe4f3f5..fbc27d07 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,6 @@ import MainRouter from './routes/MainRouter/MainRouter'; window.Kakao.init(import.meta.env.VITE_KAKAO_KEY); const queryClient = new QueryClient(); - queryClient.setDefaultOptions({ queries: { refetchOnWindowFocus: false, diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 00000000..8dc297b6 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; + +export const authRequest = { + withdrawal: async (password?: string) => + await axios.post( + '/api/members/sign-out', + { + password, + }, + {withCredentials: true}, + ), +}; diff --git a/src/api/detail.ts b/src/api/detail.ts index 28682211..f890d8ea 100644 --- a/src/api/detail.ts +++ b/src/api/detail.ts @@ -1,6 +1,7 @@ -import {MySpaces, PlacesNearby, PostReview, Reviews, ReviewsRating, Wishes, placeInfoData} from '@/types/detail'; import axios from 'axios'; +import {MySpaces, placeInfoData, PlacesNearby, PostReview, Reviews, ReviewsRating, Wishes} from '@/types/detail'; + // --------------------------- GET --------------------------- export const getPlaceInfo = async (id: number, typeId: number): Promise => { @@ -27,13 +28,10 @@ export const getReviews = async (id: number, typeId: number, title: string): Pro return response.data; }; -export const getIsWish = async (id: number, setIsWish: React.Dispatch>) => { +export const getIsWish = async (id: number) => { const response = await axios.get(`/api/wishes/${id}`, { withCredentials: true, }); - - setIsWish(response.data.data); - console.log(response.data); return response.data; diff --git a/src/api/review.ts b/src/api/review.ts new file mode 100644 index 00000000..238619bb --- /dev/null +++ b/src/api/review.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; + +export const reviewRequest = { + getMyReview: async ({pageParam}: {pageParam: number}) => { + try { + const res = await axios.get('/api/members/my-reviews', { + params: { + size: 5, + page: pageParam, + }, + withCredentials: true, + }); + console.log(res); + + return res.data; + } catch (error) { + console.log(error); + } + }, +}; diff --git a/src/api/s3.ts b/src/api/s3.ts index e10718d7..12c80a20 100644 --- a/src/api/s3.ts +++ b/src/api/s3.ts @@ -24,12 +24,12 @@ export const s3Request = { } }, - uploadImages: async (images: FileList[]) => { + uploadImages: async (images: File[]) => { try { // 이미지 당 presigned url 발급 const res = await axios.post( '/api/s3/presigned', - images.map((image) => image[0].name), + images.map((image) => image.name), ); const presignedUrls = await res.data.data.elements.map((element: PresignedUrlElement) => element.preSignedUrl); console.log('api/s3/presigned response', res); diff --git a/src/api/spaces.ts b/src/api/spaces.ts index eab1dfdb..40275d5e 100644 --- a/src/api/spaces.ts +++ b/src/api/spaces.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -import {Region} from '@/types/regionSearch'; -import {SpaceDateParams, SpaceRegionParams, SpaceResponse} from '@/types/route'; +import {ExitSpaceParams, journeyParams, PlaceParams, SpaceDateParams, SpaceRegionParams} from '@/types/route'; export const spacesRequest = { getUpcoming: async () => { @@ -13,6 +12,21 @@ export const spacesRequest = { } }, + getOutdated: async ({pageParam}: {pageParam: number}) => { + try { + const res = await axios.get('/api/members/my-spaces/outdated', { + params: { + size: 5, + page: pageParam, + }, + withCredentials: true, + }); + return res.data; + } catch (error) { + console.log(error); + } + }, + post: async () => { try { const res = await axios.post('/api/spaces', {withCredentials: true}); @@ -23,16 +37,62 @@ export const spacesRequest = { }, }; -// [GET] 지역 리스트 -export const getRegions = async (): Promise => { +// [GET] 지역 리스트 조회 +export const getRegions = async () => { const response = await axios.get(`/api/spaces/city`); return response.data.data.cities; }; -// [GET] 단일 여행 스페이스 -export const getSpace = async (spaceId: number): Promise => { - const response = await axios.get(`/api/spaces/${spaceId}`, {withCredentials: true}); - return response.data; +// [GET] 단일 여행 스페이스 조회 +export const getSpace = async (spaceId: number) => { + try { + const response = await axios.get(`/api/spaces/${spaceId}`, {withCredentials: true}); + return response.data; + } catch (error) { + console.log(error); + } +}; + +// [GET] 최근 여행 스페이스 조회 +export const getRecentSpace = async () => { + try { + const response = await axios.get(`/api/spaces/recent`, {withCredentials: true}); + return response.data; + } catch (error) { + console.log(error); + } +}; + +// [GET] 여행 일정 조회 +export const getJourneys = async (spaceId: number) => { + try { + const response = await axios.get(`/api/spaces/${spaceId}/journey`, {withCredentials: true}); + return response.data; + } catch (error) { + console.log(error); + } +}; + +// [POST] 일정 추가 +export const postPlaces = async ({spaceId, journeyId, placeIds}: PlaceParams) => { + try { + const response = await axios.post(`/api/spaces/${spaceId}/places`, {journeyId: journeyId, placeIds: placeIds}); + console.log('[SUCCESS]', response); + return response; + } catch (error) { + console.error(error); + } +}; + +// [PUT] 일정 수정 +export const putPlaces = async ({spaceId, places}: journeyParams) => { + try { + const response = await axios.put(`/api/spaces/${spaceId}/places`, {places: places}); + console.log('[SUCCESS]', response); + return response; + } catch (error) { + console.error(error); + } }; // [PUT] 지역 선택 @@ -56,3 +116,25 @@ export const putDates = async ({spaceId, startDate, endDate}: SpaceDateParams) = console.error(error); } }; + +// [PUT] 여행 나가기 +export const putExitSpace = async ({spaceId}: ExitSpaceParams) => { + try { + const response = await axios.put(`/api/spaces/${spaceId}/exit`); + console.log('[SUCCESS]', response); + return response; + } catch (error) { + console.error(error); + } +}; + +// [DELETE] 일정 삭제 +export const deletePlaces = async ({spaceId, places}: journeyParams) => { + try { + const response = await axios.delete(`/api/spaces/${spaceId}/places`, {params: {places: places}}); + console.log('[SUCCESS]', response); + return response; + } catch (error) { + console.error(error); + } +}; diff --git a/src/api/wishes.ts b/src/api/wishes.ts new file mode 100644 index 00000000..59d13ce3 --- /dev/null +++ b/src/api/wishes.ts @@ -0,0 +1,15 @@ +import axios from 'axios'; +import {Dispatch} from 'react'; + +import {DataType} from '@/types/home'; +import {Wishes} from '@/types/wish'; + +export async function getUserWishes(set: Dispatch) { + try { + const fetchData = await axios.get('/api/members/my-places'); + const data: DataType = fetchData.data; + set(data.data); + } catch (error) { + console.log(error); + } +} diff --git a/src/assets/icons/city_default.svg b/src/assets/icons/city_default.svg new file mode 100644 index 00000000..083314f6 --- /dev/null +++ b/src/assets/icons/city_default.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/spinner.gif b/src/assets/spinner.gif new file mode 100644 index 00000000..7d4a4b26 Binary files /dev/null and b/src/assets/spinner.gif differ diff --git a/src/chakra/avatarCustom.ts b/src/chakra/avatarCustom.ts index 43dd104b..1ad08f7a 100644 --- a/src/chakra/avatarCustom.ts +++ b/src/chakra/avatarCustom.ts @@ -1,29 +1,34 @@ -import { avatarAnatomy } from "@chakra-ui/anatomy"; -import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; +import {avatarAnatomy} from '@chakra-ui/anatomy'; +import {createMultiStyleConfigHelpers} from '@chakra-ui/react'; -const { definePartsStyle, defineMultiStyleConfig } = - createMultiStyleConfigHelpers(avatarAnatomy.keys); +const {definePartsStyle, defineMultiStyleConfig} = createMultiStyleConfigHelpers(avatarAnatomy.keys); const tabsVoteCard = definePartsStyle({ container: { - w: "24px", - h: "24px", - border: "1px solid #fff", + w: '24px', + h: '24px', + border: '1px solid #fff', }, excessLabel: { - w: "39px", - h: "24px", - borderRadius: "75px", - border: "1px solid #2388FF", - color: "#2388FF", - bg: "#fff", + w: '39px', + h: '24px', + borderRadius: '75px', + border: '1px solid #2388FF', + color: '#2388FF', + bg: '#fff', - fontSize: "captionSmall", - fontWeight: "captionSmall", - lineHeight: "captionSmall", + fontSize: 'captionSmall', + fontWeight: 'captionSmall', + lineHeight: 'captionSmall', + }, +}); + +const spaceAvatar = definePartsStyle({ + excessLabel: { + display: 'none !important', }, }); export const avatarTheme = defineMultiStyleConfig({ - variants: { tabsVoteCard }, + variants: {tabsVoteCard, spaceAvatar}, }); diff --git a/src/components/Auth/Input/Input.module.scss b/src/components/Auth/Input/Input.module.scss index 2ebe0eea..2e7b91eb 100644 --- a/src/components/Auth/Input/Input.module.scss +++ b/src/components/Auth/Input/Input.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .email, .sertCode, @@ -124,6 +124,7 @@ height: 7.2rem; padding: 8px; border-radius: 50%; + border: 1px solid $neutral200; &__camera { background-color: $neutral100; diff --git a/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx b/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx index abf482d2..84501ce8 100644 --- a/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx +++ b/src/components/ButtonsInAddingCandidate/SelectButton/SelectButton.tsx @@ -23,7 +23,6 @@ const SelectButton = ({data}: Propstype) => { e.preventDefault(); setIsClicked((prev) => !prev); toggleItemInNewArray(data); - console.log(data); }; useEffect(() => { diff --git a/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.module.scss b/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.module.scss index e4b928cc..ffed13fd 100644 --- a/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.module.scss +++ b/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { margin-top: 32px; @@ -20,8 +20,13 @@ position: relative; display: flex; gap: 8px; + padding: 10px 0; + + overflow-x: auto; + overflow-y: visible; &__addBox { + flex: 0 0 7.6rem; width: 7.6rem; height: 7.6rem; background-color: $neutral100; @@ -36,9 +41,7 @@ } &__box { - width: 7.6rem; - height: 7.6rem; - + flex: 0 0 7.6rem; display: flex; justify-content: center; align-items: center; @@ -62,4 +65,8 @@ } } } + + &__imageInput { + display: none; + } } diff --git a/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.tsx b/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.tsx index 963474a6..fc2cef18 100644 --- a/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.tsx +++ b/src/components/Detail/Contents/ReviewBottomSlide/ImagesWrapper/ImagesWrapper.tsx @@ -1,57 +1,86 @@ -import { IoMdCloseCircleOutline } from "react-icons/io"; -import { RiImageAddLine } from "react-icons/ri"; +import {IoMdCloseCircleOutline} from 'react-icons/io'; +import {RiImageAddLine} from 'react-icons/ri'; -import styles from "./ImagesWrapper.module.scss"; +import styles from './ImagesWrapper.module.scss'; +import CustomToast from '@/components/CustomToast/CustomToast'; + +interface imageWrapperProps { + imageUrls: string[]; + setImageUrls: React.Dispatch>; + setImageFileList: React.Dispatch>; +} + +function ImagesWrapper({imageUrls, setImageUrls, setImageFileList}: imageWrapperProps) { + const toast = CustomToast(); + + const handleImageChange = (e: React.ChangeEvent) => { + const files = e.target.files; + + if (files) { + const arr: File[] = []; + const urls: string[] = []; + + for (let i = 0; i < files.length && i < 5; i++) { + arr.push(files[i]); + + const file = files[i]; + const fileUrl = URL.createObjectURL(file); + urls.push(fileUrl); + } + + setImageUrls(urls); + setImageFileList(arr); + + if (files.length > 5) { + toast('파일의 개수가 5개를 초과하였습니다.'); + } + } + }; + + const handleRemoveImage = (index: number) => { + const updatedImageUrls = [...imageUrls]; + + updatedImageUrls.splice(index, 1); + + setImageUrls(updatedImageUrls); + }; + + console.log(imageUrls); -function ImagesWrapper() { return (
-

- 사진과 함께 리뷰를 남겨보세요. (선택) -

-

- 최대 5장까지 업로드 가능합니다. -

+

사진과 함께 리뷰를 남겨보세요. (선택)

+

최대 5장까지 업로드 가능합니다.

-
- -
-
- - # -
-
- - # -
-
- - # -
+ + + {imageUrls && + imageUrls.map((url: string, i: number) => ( +
+ handleRemoveImage(i)} + /> + # +
+ ))}
+
); } diff --git a/src/components/Detail/Contents/ReviewBottomSlide/ReviewBottomSlide.tsx b/src/components/Detail/Contents/ReviewBottomSlide/ReviewBottomSlide.tsx index 1279b494..c3e614c1 100644 --- a/src/components/Detail/Contents/ReviewBottomSlide/ReviewBottomSlide.tsx +++ b/src/components/Detail/Contents/ReviewBottomSlide/ReviewBottomSlide.tsx @@ -17,6 +17,7 @@ import StarsWrapper from './StarsWrapper/StarsWrapper'; import {ReviewBottomSlideProps} from '@/types/detail'; import {usePostReview} from '@/hooks/Detail/useReviews'; +import {s3Request} from '@/api/s3'; function ReviewBottomSlide({placeId, contentTypeId, title, slideOnClose}: ReviewBottomSlideProps) { const [isValuedInput, setIsValuedInput] = useState(false); @@ -29,6 +30,10 @@ function ReviewBottomSlide({placeId, contentTypeId, title, slideOnClose}: Review const [starCount, setStarCount] = useState(0); const [text, setText] = useState(''); const [time, setTime] = useState(new Date()); + const [imageUrls, setImageUrls] = useState([]); + const [imageFileList, setImageFileList] = useState(); + + console.log(imageFileList); const checkBeforeExit = { title: '잠깐!', @@ -51,13 +56,19 @@ function ReviewBottomSlide({placeId, contentTypeId, title, slideOnClose}: Review const postReview = usePostReview(); const handlePostReview = async () => { + const presignedUrls = await s3Request.uploadImages(imageFileList as File[]); + + presignedUrls.map((url: string, i: number) => { + presignedUrls[i] = url.split('?')[0]; + }); + await postReview.mutateAsync({ placeId, contentTypeId, title, rating: starCount, content: text, - images: [], + images: presignedUrls, visitedAt: `${time.getFullYear()}-${('00' + (time.getMonth() + 1).toString()).slice(-2)}-${( '00' + (time.getDay() + 1).toString() ).slice(-2)}`, @@ -104,7 +115,7 @@ function ReviewBottomSlide({placeId, contentTypeId, title, slideOnClose}: Review } /> - + + + + + + ); +} + +export default DeletePlacesModal; diff --git a/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx b/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx index b8c40b01..2434842a 100644 --- a/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx +++ b/src/components/Route/DraggablePlaceCard/DraggablePlaceCard.tsx @@ -3,6 +3,7 @@ import {useDrag, useDrop} from 'react-dnd'; import {IoMdMenu as MoveIcon} from 'react-icons/io'; import {RiCheckboxCircleFill as SelectedIcon} from 'react-icons/ri'; import {RiCheckboxBlankCircleLine as UnselectedIcon} from 'react-icons/ri'; +import {useNavigate} from 'react-router-dom'; import styles from '../PlaceCard/PlaceCard.module.scss'; @@ -10,41 +11,44 @@ import {Item} from '@/types/route'; import {DraggablePlaceCardProps} from '@/types/route'; function PlaceCard({ - id, + selectedId, order, name, category, address, + contentTypeId, + placeId, editMode, onSelect, moveCard, findCard, }: DraggablePlaceCardProps) { const [isChecked, setIsChecked] = useState(false); - const originalIndex = findCard(id).index; + const originalIndex = findCard(selectedId).index; + const navigate = useNavigate(); const [, drag] = useDrag( () => ({ type: 'CARD', - item: {id, originalIndex}, + item: {selectedId, originalIndex}, canDrag: () => !isChecked && editMode, end: (item, monitor) => { - const {id: droppedId, originalIndex} = item; + const {selectedId: droppedId, originalIndex} = item; const didDrop = monitor.didDrop(); if (!didDrop) { moveCard(droppedId, originalIndex); } }, }), - [id, originalIndex, moveCard, editMode, isChecked], + [selectedId, originalIndex, moveCard, editMode, isChecked], ); const [, drop] = useDrop( () => ({ accept: 'CARD', hover({id: draggedId}: Item) { - if (draggedId !== id) { - const {index: overIndex} = findCard(id); + if (draggedId !== selectedId) { + const {index: overIndex} = findCard(selectedId); moveCard(draggedId, overIndex); } }, @@ -69,7 +73,7 @@ function PlaceCard({ ))} -
+
navigate(`/detail/${placeId} ${contentTypeId}`)}> {!editMode &&
{order}
}
{editMode &&
{order}
} diff --git a/src/components/Route/MapInTrip/MapInTrip.tsx b/src/components/Route/MapInTrip/MapInTrip.tsx index 57e0a116..1383bb6c 100644 --- a/src/components/Route/MapInTrip/MapInTrip.tsx +++ b/src/components/Route/MapInTrip/MapInTrip.tsx @@ -6,16 +6,16 @@ import MapPinCommon from '@/components/CandidatesMap/MapPins/MapPinCommon'; import {MapInTripProps} from '@/types/route'; -function MapInTrip({mapRef, center}: MapInTripProps) { - const linePath = [ - {lat: 37.76437082535426, lng: 128.87675285339355}, - {lat: 37.7911054, lng: 128.9149116}, - {lat: 37.6964635, lng: 128.890664}, - ]; +function MapInTrip({mapRef, center, journeysData}: MapInTripProps) { + // TODO: active day 동선 보여주기 + const linePath = journeysData.journeys[0].places.map((place) => ({ + lat: place.place.latitude, + lng: place.place.longitude, + })); return ( - - + + {linePath.map((item, index) => (
diff --git a/src/components/Route/ObserveTarget/ObserveTarget.module.scss b/src/components/Route/ObserveTarget/ObserveTarget.module.scss new file mode 100644 index 00000000..db63f67f --- /dev/null +++ b/src/components/Route/ObserveTarget/ObserveTarget.module.scss @@ -0,0 +1,10 @@ +@use '@/sass' as *; + +.container { + display: flex; + justify-content: center; + + & > img { + width: 30px; + } +} diff --git a/src/components/Route/ObserveTarget/ObserveTarget.tsx b/src/components/Route/ObserveTarget/ObserveTarget.tsx new file mode 100644 index 00000000..c2de17ab --- /dev/null +++ b/src/components/Route/ObserveTarget/ObserveTarget.tsx @@ -0,0 +1,15 @@ +import {LegacyRef} from 'react'; + +import styles from './ObserveTarget.module.scss'; + +import spinner from '@/assets/spinner.gif'; + +function ObserveTarget({inViewRef}: {inViewRef: LegacyRef}) { + return ( +
+ 데이터 로딩 중 +
+ ); +} + +export default ObserveTarget; diff --git a/src/components/Route/RouteTabPanel/RouteTabPanel.tsx b/src/components/Route/RouteTabPanel/RouteTabPanel.tsx index 8ae88540..a473f6f7 100644 --- a/src/components/Route/RouteTabPanel/RouteTabPanel.tsx +++ b/src/components/Route/RouteTabPanel/RouteTabPanel.tsx @@ -3,122 +3,28 @@ import {useEffect, useState} from 'react'; import {HiOutlineTrash as DeleteIcon} from 'react-icons/hi'; import {RiArrowUpDownFill as MoveIcon} from 'react-icons/ri'; import {useNavigate} from 'react-router-dom'; -import {useSetRecoilState} from 'recoil'; import styles from './RouteTabPanel.module.scss'; -import AlertModal from '@/components/AlertModal/AlertModal'; import BottomSlideLeft from '@/components/BottomSlide/BottomSlideLeft'; import ZoomInIcon from '@/assets/icons/zoomIn.svg?react'; -import {isModalOpenState} from '@/recoil/vote/alertModal'; import {getSpaceId} from '@/utils/getSpaceId'; import DayMove from '../DayMove/DayMove'; import DayNavigationBar from '../DayNavigationBar/DayNavigationBar'; import DayRoute from '../DayRoute/DayRoute'; +import DeletePlacesModal from '../DeletePlacesModal/DeletePlacesModal'; import EmptyDate from '../EmptyDate/EmptyDate'; import MapInTrip from '../MapInTrip/MapInTrip'; -import {DateItem, MapInTripProps} from '@/types/route'; - -function RouteTabPanel({mapRef, center}: MapInTripProps) { - const data = { - journeys: [ - { - id: 0, - date: '2024-01-16', - places: [ - { - id: 0, - Order: 0, - place: { - id: 0, - title: '씨티 호텔', - thumbnail: - 'https://images.trvl-media.com/lodging/28000000/27440000/27434200/27434198/cb31822f.jpg?impolicy=resizecrop&rw=1200&ra=fit', - address: '강원도 강릉시', - addressDetail: '교동광장로 112', - latitude: 37.76437082535426, - longitude: 128.87675285339355, - category: '숙소', - }, - }, - { - id: 1, - Order: 1, - place: { - id: 1, - title: '동화가든', - thumbnail: - 'https://search.pstatic.net/common/?src=http%3A%2F%2Fblogfiles.naver.net%2FMjAyMzEyMjRfMjgg%2FMDAxNzAzMzk1OTQ3NzIx.PD8Sif-ZdTdc9tugl9qh9Izb91v0tK_OD1IJPvgVEbAg.oc9JBSNBPc6WjsJOFhCcXXBoG2Qg318fhOveoAyqbvog.JPEG.rhwpgus90%2FIMG_3224.JPG', - address: '강원도 강릉시', - addressDetail: '초당순두부길77번길 15', - latitude: 37.7911054, - longitude: 128.9149116, - category: '맛집', - }, - }, - { - id: 2, - Order: 2, - place: { - id: 2, - title: '테라로사', - thumbnail: - 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20200422_278%2F1587531042172TgXbr_JPEG%2FV-ta0vOWwlwKQkmnI-B9s7ja.jpg', - address: '강원도 강릉시', - addressDetail: '구정면 현천길 7', - latitude: 37.6964635, - longitude: 128.890664, - category: '카페', - }, - }, - ], - }, - { - id: 1, - date: '2024-01-17', - places: [ - { - id: 3, - Order: 0, - place: { - id: 3, - title: '안목해변', - thumbnail: - 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20190130_26%2F1548818549792K262M_JPEG%2FyOtLXHFaaCdCC6c9frIgwJTB.jpeg.jpg', - address: '강원도 강릉시', - addressDetail: '창해로14번길 20-1', - latitude: 37.7725926, - longitude: 128.9473204, - category: '관광', - }, - }, - { - id: 4, - Order: 1, - place: { - id: 4, - title: '카페오션스강릉버거', - thumbnail: - 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20240102_48%2F17041673424734KIm1_JPEG%2F%25B0%25AD%25B8%25AA_%25B8%25C0%25C1%25FD_%25286%2529.jpg', - address: '강원도 강릉시', - addressDetail: '해안로 355', - latitude: 37.8952651, - longitude: 128.8292485, - category: '맛집', - }, - }, - ], - }, - ], - }; +import {DateItem, Journey, MapInTripProps} from '@/types/route'; +function RouteTabPanel({mapRef, center, journeysData}: MapInTripProps) { const [isEditMode, setIsEditMode] = useState(false); const [selectedPlaces, setSelectedPlaces] = useState([]); const {isOpen, onOpen, onClose} = useDisclosure(); - const setIsModalOpen = useSetRecoilState(isModalOpenState); + const {isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose} = useDisclosure(); const handleEditMode = () => { setIsEditMode(!isEditMode); @@ -134,10 +40,6 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) { } }; - const deletePlaces = (placeList: string[]) => { - console.log('삭제', placeList); - }; - useEffect(() => { console.log(selectedPlaces); }, [selectedPlaces]); @@ -145,18 +47,18 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) { const navigate = useNavigate(); const spaceId = getSpaceId(); - if (!data.journeys || data.journeys.length === 0) { + if (!journeysData.journeys || journeysData.journeys.length === 0) { return ; } - const dateList: DateItem[] = data.journeys.map((journey) => ({ + const dateList: DateItem[] = journeysData.journeys.map((journey: Journey) => ({ date: journey.date, })); return (
- + @@ -164,10 +66,10 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) {
- {data.journeys && - data.journeys.map((journey, index) => ( + {journeysData.journeys && + journeysData.journeys.map((journey: Journey, index: number) => ( 날짜 이동 - @@ -195,13 +97,7 @@ function RouteTabPanel({mapRef, center}: MapInTripProps) { onClose={onClose} children={} /> - - deletePlaces(selectedPlaces)} - /> +
); } diff --git a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx index 9d99fce0..50134c90 100644 --- a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx +++ b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx @@ -5,34 +5,44 @@ import {useNavigate} from 'react-router-dom'; import styles from './DateFilter.module.scss'; import {ForSearchType} from '@/types/home'; +import {WishFilterType} from '@/types/wish'; interface PropsType { - forSearch: ForSearchType; + forSearch?: ForSearchType | undefined; + wishesFilter?: WishFilterType | undefined; } -function DateFilter({forSearch}: PropsType) { +function DateFilter({forSearch = undefined, wishesFilter = undefined}: PropsType) { const [click, setClick] = useState(false); const filterData = ['등록순', '이름순', '인기순']; const navigate = useNavigate(); + const sort = forSearch ? forSearch.sort : wishesFilter?.sort; function handleModal() { setClick((prev) => !prev); } function selectSort(sort: string) { - navigate( - `/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${forSearch.location}&sort=${sort}&hot=${forSearch.hot}&placeID=${forSearch.placeID}&tripDate=${forSearch.tripDate}`, - ); + if (forSearch) { + navigate( + `/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${forSearch.location}&sort=${sort}&hot=${forSearch.hot}&placeID=${forSearch.placeID}&tripDate=${forSearch.tripDate}`, + ); + } + if (wishesFilter) { + navigate( + `/wish?category=${wishesFilter.category}&location=${wishesFilter.location}&placeID=${wishesFilter.placeID}&tripDate=${wishesFilter.tripDate}&sort=${sort}`, + ); + } } useEffect(() => { setClick(false); - }, [forSearch.sort]); + }, [forSearch?.sort]); return (
- {forSearch.sort} + {sort}
{filterData.map((data) => ( { selectSort(data); }} + key={data} > {data} diff --git a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx index 95e489ec..9def776d 100644 --- a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx +++ b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx @@ -6,12 +6,14 @@ import styles from './LocationFilter.module.scss'; import LocationFliterPage from './LocationFliterPage/LocationFliterPage'; import {ForSearchType} from '@/types/home'; +import {WishFilterType} from '@/types/wish'; interface PropsType { - forSearch: ForSearchType; + forSearch?: ForSearchType | undefined; + wishesFilter?: WishFilterType | undefined; } -function LocationFilter({forSearch}: PropsType) { +function LocationFilter({forSearch = undefined, wishesFilter = undefined}: PropsType) { const [click, setClick] = useState(true); const [buttonName, setButtonName] = useState('전체 지역'); @@ -20,13 +22,20 @@ function LocationFilter({forSearch}: PropsType) { } useEffect(() => { - const datas = forSearch.location.split(' '); - if (datas[0] === '전국') { - setButtonName('전체 지역'); + let locationData: string[] | undefined; + if (forSearch) { + locationData = forSearch.location.split(' '); } else { - setButtonName(datas[0]); + locationData = wishesFilter?.location.split(' '); } - }, [forSearch.location]); + if (locationData) { + if (locationData[0] === '전국') { + setButtonName('전체 지역'); + } else { + setButtonName(locationData[0]); + } + } + }, [forSearch?.location, wishesFilter?.location]); return ( <> @@ -42,7 +51,7 @@ function LocationFilter({forSearch}: PropsType) { {buttonName}
- + ); } diff --git a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx index 6f39db95..4e36cea7 100644 --- a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx +++ b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFliterPage/LocationFliterPage.tsx @@ -9,10 +9,12 @@ import PopularList from './PopularList/PopularList'; import SelectLocation from './SelectLocation/SelectLocation'; import {ForSearchType} from '@/types/home'; +import {WishFilterType} from '@/types/wish'; interface PropsType { click: boolean; - forSearch: ForSearchType; + forSearch: ForSearchType | undefined; + wishesFilter?: WishFilterType | undefined; handleClick: () => void; } @@ -21,7 +23,7 @@ interface AreaDataType { sigunguCode: number; } -function LocationFliterPage({forSearch, click, handleClick}: PropsType) { +function LocationFliterPage({forSearch, wishesFilter, click, handleClick}: PropsType) { const [area, setArea] = useState('전국'); const [areaData, setAreaData] = useState(); const [sigungu, setSigungu] = useState('전체 지역'); @@ -31,20 +33,41 @@ function LocationFliterPage({forSearch, click, handleClick}: PropsType) { const vh = window.innerHeight / 100; useEffect(() => { - const locationData = forSearch.location.split(' '); - if (locationData[0] === '전국') { - setArea('전국'); - setSigungu('전체 지역'); - } else { - setArea(locationData[0]); - setSigungu(locationData[1]); + if (forSearch) { + const locationData = forSearch.location.split(' '); + if (locationData[0] === '전국') { + setArea('전국'); + setSigungu('전체 지역'); + } else { + setArea(locationData[0]); + setSigungu(locationData[1]); + } } - }, [forSearch.location]); + if (wishesFilter) { + const locationData = wishesFilter.location.split(' '); + if (locationData[0] === '전국') { + setArea('전국'); + setSigungu('전체 지역'); + } else { + setArea(locationData[0]); + setSigungu(locationData[1]); + } + } + }, [forSearch?.location, wishesFilter?.location]); function submit() { - navigate( - `/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${area} ${sigungu}&sort=${forSearch.sort}&hot=${forSearch.hot}&placeID=${forSearch.placeID}&tripDate=${forSearch.tripDate}`, - ); + if (forSearch) { + navigate( + `/search?keyword=${forSearch.keyword}&category=${forSearch.category}&map=${forSearch.map}&location=${area} ${sigungu}&sort=${forSearch.sort}&hot=${forSearch.hot}&placeID=${forSearch.placeID}&tripDate=${forSearch.tripDate}`, + ); + } + console.log(wishesFilter); + + if (wishesFilter) { + navigate( + `/wishes?category=${wishesFilter.category}&location=${area} ${sigungu}&placeID=${wishesFilter.placeID}&tripDate=${wishesFilter.tripDate}&sort=${wishesFilter.sort}`, + ); + } handleClick(); } @@ -62,9 +85,21 @@ function LocationFliterPage({forSearch, click, handleClick}: PropsType) { - + +
diff --git a/src/components/TripSpace/FriendList/FriendList.module.scss b/src/components/TripSpace/FriendList/FriendList.module.scss index 9c5bf84d..a788f5cb 100644 --- a/src/components/TripSpace/FriendList/FriendList.module.scss +++ b/src/components/TripSpace/FriendList/FriendList.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { display: flex; @@ -13,7 +13,7 @@ display: flex; flex-direction: column; gap: 16px; - max-height: calc(4rem * 5 + 16px * 4); + height: calc(4rem * 5 + 16px * 4); overflow-y: auto; &::-webkit-scrollbar { display: none; diff --git a/src/components/TripSpace/FriendList/FriendList.tsx b/src/components/TripSpace/FriendList/FriendList.tsx index 843c1de1..81e6b694 100644 --- a/src/components/TripSpace/FriendList/FriendList.tsx +++ b/src/components/TripSpace/FriendList/FriendList.tsx @@ -2,25 +2,18 @@ import {Avatar} from '@chakra-ui/react'; import styles from './FriendList.module.scss'; -import {FriendListProps} from '@/types/friendList'; -function FriendList({users}: FriendListProps) { +import {Member, Members} from '@/types/route'; +function FriendList({members}: Members) { return (
-

여행을 함께하는 친구 {users.length}명

+

여행을 함께하는 친구 {members.length}명

- {users.map((user, index) => ( -
- -

{user.name}

-
- ))} - {/* 실제 api 연동 - {users.map((user, index) => ( -
- + {members.map((user: Member) => ( +
+

{user.nickname}

- ))} */} + ))}
); diff --git a/src/components/User/EditProfileForm/EditProfileForm.tsx b/src/components/User/EditProfileForm/EditProfileForm.tsx index b3c904f5..ad896652 100644 --- a/src/components/User/EditProfileForm/EditProfileForm.tsx +++ b/src/components/User/EditProfileForm/EditProfileForm.tsx @@ -40,6 +40,8 @@ function EditProfileForm({data}: {data: GetUserProp | undefined}) { if (dirtyFields.image) { const presignedUrl = await s3Request.uploadImage(image as FileList); + console.log(presignedUrl); + const res = await axios.put('/api/members/my-info', { nickname, profile: presignedUrl.split('?')[0], diff --git a/src/components/User/Mywork/Mywork.module.scss b/src/components/User/Mywork/Mywork.module.scss index 055963d1..a073ca43 100644 --- a/src/components/User/Mywork/Mywork.module.scss +++ b/src/components/User/Mywork/Mywork.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { display: flex; @@ -35,6 +35,6 @@ } .title { - @include typography(subTitle); + @include typography(tabLabel); } } diff --git a/src/components/User/Profile/Profile.module.scss b/src/components/User/Profile/Profile.module.scss index 32ea1f7b..cde4b547 100644 --- a/src/components/User/Profile/Profile.module.scss +++ b/src/components/User/Profile/Profile.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { } @@ -13,6 +13,7 @@ width: 7.2rem; height: 7.2rem; border-radius: 50%; + border: 1px solid $neutral200; background-position: center; background-repeat: no-repeat; diff --git a/src/components/User/Profile/Profile.tsx b/src/components/User/Profile/Profile.tsx index fa978fde..5d86025c 100644 --- a/src/components/User/Profile/Profile.tsx +++ b/src/components/User/Profile/Profile.tsx @@ -15,7 +15,7 @@ function Profile({data}: ProfileProps) {
+ ); +} + +export default WishesHeader; diff --git a/src/hooks/Detail/useWish.ts b/src/hooks/Detail/useWish.ts index b8f17fb0..feacf302 100644 --- a/src/hooks/Detail/useWish.ts +++ b/src/hooks/Detail/useWish.ts @@ -1,11 +1,13 @@ -import {deleteWishes, getIsWish, postWishes} from '@/api/detail'; import {useSuspenseQuery} from '@tanstack/react-query'; + +import {deleteWishes, getIsWish, postWishes} from '@/api/detail'; + import {useCustomMutation} from '../Votes/vote'; -export const useGetIsWish = (id: number, setIsWish: React.Dispatch>) => { +export const useGetIsWish = (id: number) => { return useSuspenseQuery({ queryKey: ['isWish', id], - queryFn: () => getIsWish(id, setIsWish), + queryFn: () => getIsWish(id), }); }; diff --git a/src/hooks/Spaces/space.ts b/src/hooks/Spaces/space.ts index 467baef0..5a9377d9 100644 --- a/src/hooks/Spaces/space.ts +++ b/src/hooks/Spaces/space.ts @@ -1,8 +1,18 @@ import {useMutation, useQueryClient, useSuspenseQuery} from '@tanstack/react-query'; -import {getRegions, getSpace, putDates, putRegions} from '@/api/spaces'; +import { + deletePlaces, + getJourneys, + getRecentSpace, + getRegions, + getSpace, + postPlaces, + putDates, + putExitSpace, + putRegions, +} from '@/api/spaces'; -// [GET] 지역 리스트 +// [GET] 지역 리스트 조회 export const useGetRegions = () => { return useSuspenseQuery({ queryKey: ['spaces', 'city'], @@ -10,7 +20,7 @@ export const useGetRegions = () => { }); }; -// [GET] 단일 여행 스페이스 +// [GET] 단일 여행 스페이스 조회 export const useGetSpace = (spaceId: number) => { return useSuspenseQuery({ queryKey: ['spaces', spaceId], @@ -18,6 +28,44 @@ export const useGetSpace = (spaceId: number) => { }); }; +// [GET] 최근 여행 스페이스 조회 +export const useGetRecentSpace = () => { + return useSuspenseQuery({ + queryKey: ['spaces', 'recent'], + queryFn: () => getRecentSpace(), + }); +}; + +// [GET] 여행 일정 조회 +export const useGetJourneys = (spaceId: number) => { + return useSuspenseQuery({ + queryKey: ['spaces', spaceId, 'journeys'], + queryFn: () => getJourneys(spaceId), + }); +}; + +// [POST] 일정 추가 +export const usePostPlaces = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: postPlaces, + onSuccess: () => { + return queryClient.invalidateQueries({queryKey: ['spaces', 'places']}); + }, + }); +}; + +// [PUT] 일정 수정 +export const usePutPlaces = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: putDates, + onSuccess: () => { + return queryClient.invalidateQueries({queryKey: ['spaces', 'places']}); + }, + }); +}; + // [PUT] 날짜 선택 export const usePutRegions = () => { const queryClient = useQueryClient(); @@ -39,3 +87,25 @@ export const usePutDates = () => { }, }); }; + +// [PUT] 여행 나가기 +export const usePutExitSpace = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: putExitSpace, + onSuccess: () => { + return queryClient.invalidateQueries({queryKey: ['spaces', 'exit']}); + }, + }); +}; + +// [DELETE] 일정 삭제 +export const useDeleteExitSpace = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deletePlaces, + onSuccess: () => { + return queryClient.invalidateQueries({queryKey: ['spaces', 'places']}); + }, + }); +}; diff --git a/src/hooks/Spaces/useSpaces.ts b/src/hooks/Spaces/useSpaces.ts index ee36bb16..9d53c2a3 100644 --- a/src/hooks/Spaces/useSpaces.ts +++ b/src/hooks/Spaces/useSpaces.ts @@ -1,14 +1,28 @@ -import {useMutation, useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'; +import {useInfiniteQuery, useMutation, useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'; import {spacesRequest} from '@/api/spaces'; import {GetUpcomingProp} from '@/types/sidebar'; -function useGetSpaces(isSideOpen: boolean): UseQueryResult { +function useGetSpaces(enabled: boolean): UseQueryResult { return useQuery({ queryKey: ['spaces'], queryFn: spacesRequest.getUpcoming, - enabled: isSideOpen, + enabled, + }); +} + +function useGetSpacesOut() { + return useInfiniteQuery({ + queryKey: ['spacesOut'], + queryFn: spacesRequest.getOutdated, + initialPageParam: 0, + getNextPageParam: (lastPage, _, lastPageParam) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPageParam + 1; + }, }); } @@ -22,4 +36,4 @@ function usePostSpace() { }); } -export {useGetSpaces, usePostSpace}; +export {useGetSpaces, useGetSpacesOut, usePostSpace}; diff --git a/src/hooks/User/useMyReview.ts b/src/hooks/User/useMyReview.ts new file mode 100644 index 00000000..03b191b2 --- /dev/null +++ b/src/hooks/User/useMyReview.ts @@ -0,0 +1,17 @@ +import {useInfiniteQuery} from '@tanstack/react-query'; + +import {reviewRequest} from '@/api/review'; + +export function useGetMyReview() { + return useInfiniteQuery({ + queryKey: ['myReview'], + queryFn: reviewRequest.getMyReview, + initialPageParam: 0, + getNextPageParam: (lastPage, _, lastPageParam) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPageParam + 1; + }, + }); +} diff --git a/src/hooks/useGoLogin.ts b/src/hooks/useGoLogin.ts new file mode 100644 index 00000000..386f1508 --- /dev/null +++ b/src/hooks/useGoLogin.ts @@ -0,0 +1,12 @@ +import {useNavigate} from 'react-router-dom'; + +function useGoLogin() { + const navigate = useNavigate(); + + const useGoLogin = () => { + navigate('/auth/login'); + }; + + return useGoLogin; +} +export default useGoLogin; diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts new file mode 100644 index 00000000..95aa8565 --- /dev/null +++ b/src/hooks/useInfiniteScroll.ts @@ -0,0 +1,29 @@ +import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'; +import {useEffect, useState} from 'react'; +import {useInView} from 'react-intersection-observer'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const useInfiniteScroll = (infiniteQueryFn: () => UseInfiniteQueryResult, Error>) => { + const {data, fetchNextPage} = infiniteQueryFn(); + const [hasNextData, setHasNextData] = useState(true); + const {ref: inViewRef, inView} = useInView({ + rootMargin: '0px 0px -56px 0px', + threshold: 1, + }); + + useEffect(() => { + if (data) { + const {last} = data.pages.at(-1).data; + + if (inView && !last) { + fetchNextPage(); + } + if (last) { + setHasNextData(false); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inView]); + + return [data, hasNextData, inViewRef] as const; +}; diff --git a/src/mocks/handlers/auth.ts b/src/mocks/handlers/auth.ts index 8a0d3431..6803db2d 100644 --- a/src/mocks/handlers/auth.ts +++ b/src/mocks/handlers/auth.ts @@ -1,4 +1,4 @@ -import { http, HttpResponse, PathParams } from "msw"; +import {http, HttpResponse, PathParams} from 'msw'; interface LoginBody { email: string; @@ -11,7 +11,7 @@ interface Email { interface Sert { email: string; - token: string; + code: string; } interface Form { @@ -27,32 +27,30 @@ interface Find { newPassword: string; } -const UserDummy = [ - { email: "test@naver.com", password: "asdasd123#", nickname: "테스트계정" }, -]; +const UserDummy = [{email: 'test@naver.com', password: 'asdasd123#', nickname: '테스트계정'}]; const FormDummy = [ { - email: "test2@naver.com", - password: "asdasd123#", - nickname: "테스트", - profile: "https://avatars.githubusercontent.com/u/100336573?v=4", - token: "asdf1234", + email: 'test2@naver.com', + password: 'asdasd123#', + nickname: '테스트', + profile: 'https://avatars.githubusercontent.com/u/100336573?v=4', + code: 'asdf1234', }, ]; export const auth = [ /* ----------------------------------- <로그인> ---------------------------------- */ - http.post("/api/login", async ({ request }) => { - const { email, password } = await request.json(); + http.post('/api/login', async ({request}) => { + const {email, password} = await request.json(); // 로그인 유저 정보 일치 if (email === UserDummy[0].email && password === UserDummy[0].password) { return HttpResponse.json(null, { status: 200, headers: { - "Set-Cookie": "access-token=msw-access, refresh-token=msw-refresh", + 'Set-Cookie': 'access-token=msw-access, refresh-token=msw-refresh', }, }); } @@ -62,8 +60,8 @@ export const auth = [ { status: 400, response_code: 401, - detail: "이메일 또는 비밀번호를 확인해주세요.", - issue: "tripvote.site/error", + detail: '이메일 또는 비밀번호를 확인해주세요.', + issue: 'tripvote.site/error', }, { status: 400, @@ -74,51 +72,48 @@ export const auth = [ /* ------------------------------------ <회원가입> ----------------------------------- */ /* -------------------------------- 이메일 인증 요청 ------------------------------- */ - http.post( - "/api/auth/register/send-email", - async ({ request }) => { - const { email } = await request.json(); - - // 이메일 중복체크 실패 - if (email === UserDummy[0].email) { - return HttpResponse.json({ - status: 200, - response_code: 401, - detail: "이미 가입된 이메일입니다.", - issue: "tripvote.site/error", - }); - } - - // 이메일 중복체크 성공 - return HttpResponse.json({ status: 200 }); - }, - ), + http.post('/api/auth/register/send-email', async ({request}) => { + const {email} = await request.json(); + + // 이메일 중복체크 실패 + if (email === UserDummy[0].email) { + return HttpResponse.json({ + status: 200, + responseCode: 101, + detail: '이미 가입된 이메일입니다.', + issue: 'tripvote.site/error', + }); + } + + // 이메일 중복체크 성공 + return HttpResponse.json({status: 200}); + }), /* ------------------------------ 이메일 인증 완료 버튼 ------------------------------ */ - http.post( - "/api/auth/register/check-token", - async ({ request }) => { - const { email, token } = await request.json(); - - // 인증 코드 일치 - if (email === FormDummy[0].email && token === FormDummy[0].token) { - return HttpResponse.json({ - status: 200, - }); - } - - // 인증 코드 불일치 + http.post('/api/auth/register/check-token', async ({request}) => { + const {email, code} = await request.json(); + + // 인증 코드 일치 + if (email === FormDummy[0].email && code === FormDummy[0].code) { return HttpResponse.json({ status: 200, - response_code: 403, - detail: "인증코드가 올바르지 않습니다.", - issue: "tripvote.site/error", + data: { + token: 1234, + }, }); - }, - ), + } + + // 인증 코드 불일치 + return HttpResponse.json({ + status: 200, + responseCode: 203, + detail: '인증코드가 올바르지 않습니다.', + issue: 'tripvote.site/error', + }); + }), /* ------------------------------ 시작하기 버튼 (form 제출) ----------------------------- */ - http.post("/api/auth/register", async () => { + http.post('/api/auth/register', async () => { // 회원가입 성공 return HttpResponse.json({ status: 200, @@ -128,37 +123,31 @@ export const auth = [ /* --------------------------------- 비밀번호 찾기 -------------------------------- */ /* -------------------------- 비밀번호 찾기 (이메일 인증코드 발송)송------------------------- */ - http.post( - "/api/auth/modify/lost-password/send-email", - async () => { - return HttpResponse.json({ - status: 200, - }); - }, - ), + http.post('/api/auth/modify/lost-password/send-email', async () => { + return HttpResponse.json({ + status: 200, + }); + }), /* ---------------------------- 비밀번호 찾기 (인증코드 제출)출--------------------------- */ - http.post( - "/api/auth/modify/lost-password/check-token", - async ({ request }) => { - const { token } = await request.json(); - - if (token === "asdf1234") - return HttpResponse.json({ - status: 200, - }); + http.post('/api/auth/modify/lost-password/check-token', async ({request}) => { + const {token} = await request.json(); + if (token === 'asdf1234') return HttpResponse.json({ status: 200, - response_code: 403, - detail: "인증코드가 올바르지 않습니다.", - issue: "tripvote.site/error", }); - }, - ), + + return HttpResponse.json({ + status: 200, + response_code: 403, + detail: '인증코드가 올바르지 않습니다.', + issue: 'tripvote.site/error', + }); + }), /* ------------------------- 비밀번호 찾기 (새로운 비밀번호로 변경)경------------------------- */ - http.post("/api/auth/modify/lost-password", () => { + http.post('/api/auth/modify/lost-password', () => { return HttpResponse.json({ status: 200, }); @@ -166,13 +155,13 @@ export const auth = [ /* --------------------------------- <로그아웃> --------------------------------- */ - http.post("/api/logout", () => { - console.log("로그아웃"); + http.post('/api/logout', () => { + console.log('로그아웃'); return new HttpResponse(null, { status: 200, headers: { - "Set-Cookie": "tmpToken=abc;Path=/;Max-Age=0", + 'Set-Cookie': 'tmpToken=abc;Path=/;Max-Age=0', }, }); }), diff --git a/src/pages/Auth/Withdrawal/Withdrawal.module.scss b/src/pages/Auth/Withdrawal/Withdrawal.module.scss index 3437561d..fa083d14 100644 --- a/src/pages/Auth/Withdrawal/Withdrawal.module.scss +++ b/src/pages/Auth/Withdrawal/Withdrawal.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { color: $neutral900; @@ -16,7 +16,7 @@ margin-left: 4px; vertical-align: text-top; - background-image: url("@/assets/icons/crying_imoji.svg"); + background-image: url('@/assets/icons/crying_imoji.svg'); background-position: center; background-repeat: no-repeat; } @@ -43,5 +43,35 @@ margin-bottom: 8px; } } + + .input { + width: 100%; + margin-top: 6px; + margin-bottom: 24px; + padding: 16px; + border: 1px solid $neutral200; + border-radius: 8px; + + @include typography(bodyLarge); + color: $neutral900; + + &:focus { + outline: none; + border-width: 1px; + border-color: $primary300; + } + + &::placeholder { + color: $neutral400; + } + + &.error { + border-color: $danger300; + } + + &:disabled { + color: $neutral300; + } + } } } diff --git a/src/pages/Auth/Withdrawal/Withdrawal.tsx b/src/pages/Auth/Withdrawal/Withdrawal.tsx index 38585db5..77d2235f 100644 --- a/src/pages/Auth/Withdrawal/Withdrawal.tsx +++ b/src/pages/Auth/Withdrawal/Withdrawal.tsx @@ -1,11 +1,66 @@ -import styles from "./Withdrawal.module.scss"; +import {useForm} from 'react-hook-form'; +import {useNavigate} from 'react-router-dom'; +import {useSetRecoilState} from 'recoil'; -import AuthButton from "@/components/Auth/Button/AuthButton"; -import Header from "@/components/Auth/Header/Header"; +import styles from './Withdrawal.module.scss'; + +import {useGetMyInfo} from '@/hooks/User/useUser'; + +import AlertModal from '@/components/AlertModal/AlertModal'; +import AuthButton from '@/components/Auth/Button/AuthButton'; +import Header from '@/components/Auth/Header/Header'; +import CustomToast from '@/components/CustomToast/CustomToast'; + +import {authRequest} from '@/api/auth'; +import {isModalOpenState} from '@/recoil/vote/alertModal'; + +import {AuthForm} from '@/types/auth'; function Withdrawal() { + const {data} = useGetMyInfo(true); + const { + register, + formState: {dirtyFields}, + watch, + resetField, + } = useForm({ + mode: 'onChange', + defaultValues: { + password: '', + }, + }); + const password = watch('password'); + const setIsModalOpen = useSetRecoilState(isModalOpenState); + const showToast = CustomToast(); + const navigate = useNavigate(); + + const signout = async () => { + try { + const res = + data?.data.provider === 'NONE' ? await authRequest.withdrawal(password) : await authRequest.withdrawal(); + + console.log(res); + + if (res.data.responseCode === 206) { + showToast('비밀번호가 일치하지 않습니다.'); + resetField('password'); + setIsModalOpen(false); + return; + } + + setIsModalOpen(false); + navigate('/'); + } catch (error) { + console.log(error); + // 백엔드 validation 오류 - 리팩토링 시 responseCode 조건 걸 예정 + showToast('비밀번호가 일치하지 않습니다.'); + resetField('password'); + setIsModalOpen(false); + } + }; + const onClickButton = () => { - // 회원탈퇴 api + setIsModalOpen(true); }; return ( @@ -34,11 +89,27 @@ function Withdrawal() {
  • 4. 내가 작성한 리뷰
  • - + )} + + + +
    diff --git a/src/pages/Calendar/Calendar.tsx b/src/pages/Calendar/Calendar.tsx index 95abd16c..e25403a7 100644 --- a/src/pages/Calendar/Calendar.tsx +++ b/src/pages/Calendar/Calendar.tsx @@ -1,24 +1,39 @@ -import { Button } from "@chakra-ui/react"; -import { addYears, format } from "date-fns"; -import ko from "date-fns/locale/ko"; -import { useEffect, useState } from "react"; -import DatePicker, { registerLocale } from "react-datepicker"; -import { BsCalendarCheck as CalendarIcon } from "react-icons/bs"; +import {Button} from '@chakra-ui/react'; +import {addYears, format} from 'date-fns'; +import ko from 'date-fns/locale/ko'; +import {useEffect, useState} from 'react'; +import DatePicker, {registerLocale} from 'react-datepicker'; +import {BsCalendarCheck as CalendarIcon} from 'react-icons/bs'; +import {useParams} from 'react-router-dom'; -import "react-datepicker/dist/react-datepicker.css"; -import styles from "./Calendar.module.scss"; +import 'react-datepicker/dist/react-datepicker.css'; +import styles from './Calendar.module.scss'; -import SelectHeader from "@/components/TripSpace/SelectHeader/SelectHeader"; +import {useGetSpace, usePutDates} from '@/hooks/Spaces/space'; -import { printDayNight } from "@/utils/printDayNight"; +import SelectHeader from '@/components/TripSpace/SelectHeader/SelectHeader'; + +import {createDate} from '@/utils/formatDate'; +import {printDayNight} from '@/utils/printDayNight'; function Calendar() { - registerLocale("ko", ko); + registerLocale('ko', ko); const today = new Date(); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + const {id} = useParams(); + const {data: spaceData} = useGetSpace(Number(id)); + const putDates = usePutDates(); + const [startDate, setStartDate] = useState(createDate(spaceData?.data?.startDate)); + const [endDate, setEndDate] = useState(createDate(spaceData?.data?.endDate)); + + const editDates = async () => { + await putDates.mutateAsync({ + spaceId: Number(id), + startDate: format(startDate!, 'yyyy-MM-dd'), + endDate: format(endDate!, 'yyyy-MM-dd'), + }); + }; - // TODO: 캘린더에서 날짜 바꿀 경우 Day별 일정 처리 + // TODO: 캘린더에서 날짜 바꿀 경우 Day별 일정 처리 모달 띄우기 // 1. 박 수가 같은 경우 일정 그대로 유지 // 2. 박 수가 줄어들 경우 줄어든 박 수만큼의 일정 마지막 날로 합치기 (하루 동선 Max 30개) // 3. 박 수가 늘어날 경우 나머지 빈 일정으로 생성 @@ -36,13 +51,13 @@ function Calendar() { }; useEffect(() => { - if (startDate) console.log("시작", format(startDate, "yyyy/MM/dd")); - if (endDate) console.log("끝", format(endDate, "yyyy/MM/dd")); + if (startDate) console.log('시작', startDate, format(startDate, 'yyyy/MM/dd')); + if (endDate) console.log('끝', endDate, format(endDate, 'yyyy/MM/dd')); }, [startDate, endDate]); return (
    - +
    {/* FIXME : date picker 수정사항 1. 달력 첫째주에 지난 달 날짜 보이도록 @@ -57,26 +72,20 @@ function Calendar() { maxDate={addYears(today, 3)} locale={ko} monthsShown={36} - dateFormatCalendar="yyyy년 M월" + dateFormatCalendar='yyyy년 M월' renderDayContents={(day) => {day}} />
    - - {startDate ? format(startDate, "M월 d일") : "시작일"} + + {startDate ? format(startDate, 'M월 d일') : '시작일'} - - {endDate ? format(endDate, "M월 d일") : "종료일"} - {startDate && endDate && ( - {printDayNight(startDate, endDate)} - )} + {endDate ? format(endDate, 'M월 d일') : '종료일'} + {startDate && endDate && {printDayNight(startDate, endDate)}}
    -
    diff --git a/src/pages/MapZoomIn/MapZoomIn.tsx b/src/pages/MapZoomIn/MapZoomIn.tsx index 2b890d8f..777b0763 100644 --- a/src/pages/MapZoomIn/MapZoomIn.tsx +++ b/src/pages/MapZoomIn/MapZoomIn.tsx @@ -12,14 +12,14 @@ function MapZoomIn() { const data = { journeys: [ { - id: 0, + journeyId: 0, date: '2024-01-16', places: [ { - id: 0, - Order: 0, + selectedId: 0, + order: 0, place: { - id: 0, + placeId: 0, title: '씨티 호텔', thumbnail: 'https://images.trvl-media.com/lodging/28000000/27440000/27434200/27434198/cb31822f.jpg?impolicy=resizecrop&rw=1200&ra=fit', @@ -28,13 +28,14 @@ function MapZoomIn() { latitude: 37.76437082535426, longitude: 128.87675285339355, category: '숙소', + contentTypeId: 0, }, }, { - id: 1, - Order: 1, + selectedId: 1, + order: 1, place: { - id: 1, + placeId: 1, title: '동화가든', thumbnail: 'https://search.pstatic.net/common/?src=http%3A%2F%2Fblogfiles.naver.net%2FMjAyMzEyMjRfMjgg%2FMDAxNzAzMzk1OTQ3NzIx.PD8Sif-ZdTdc9tugl9qh9Izb91v0tK_OD1IJPvgVEbAg.oc9JBSNBPc6WjsJOFhCcXXBoG2Qg318fhOveoAyqbvog.JPEG.rhwpgus90%2FIMG_3224.JPG', @@ -43,13 +44,14 @@ function MapZoomIn() { latitude: 37.7911054, longitude: 128.9149116, category: '맛집', + contentTypeId: 0, }, }, { - id: 2, - Order: 2, + selectedId: 2, + order: 2, place: { - id: 2, + placeId: 2, title: '테라로사', thumbnail: 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20200422_278%2F1587531042172TgXbr_JPEG%2FV-ta0vOWwlwKQkmnI-B9s7ja.jpg', @@ -58,19 +60,20 @@ function MapZoomIn() { latitude: 37.6964635, longitude: 128.890664, category: '카페', + contentTypeId: 0, }, }, ], }, { - id: 1, + journeyId: 1, date: '2024-01-17', places: [ { - id: 3, - Order: 0, + selectedId: 3, + order: 0, place: { - id: 3, + placeId: 3, title: '안목해변', thumbnail: 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20190130_26%2F1548818549792K262M_JPEG%2FyOtLXHFaaCdCC6c9frIgwJTB.jpeg.jpg', @@ -79,13 +82,14 @@ function MapZoomIn() { latitude: 37.7725926, longitude: 128.9473204, category: '관광', + contentTypeId: 0, }, }, { - id: 4, - Order: 1, + selectedId: 4, + order: 1, place: { - id: 4, + placeId: 4, title: '카페오션스강릉버거', thumbnail: 'https://search.pstatic.net/common/?src=https%3A%2F%2Fldb-phinf.pstatic.net%2F20240102_48%2F17041673424734KIm1_JPEG%2F%25B0%25AD%25B8%25AA_%25B8%25C0%25C1%25FD_%25286%2529.jpg', @@ -94,6 +98,7 @@ function MapZoomIn() { latitude: 37.8952651, longitude: 128.8292485, category: '맛집', + contentTypeId: 0, }, }, ], diff --git a/src/pages/RegionSearch/RegionSearch.tsx b/src/pages/RegionSearch/RegionSearch.tsx index ce98e216..7e4c1784 100644 --- a/src/pages/RegionSearch/RegionSearch.tsx +++ b/src/pages/RegionSearch/RegionSearch.tsx @@ -1,5 +1,6 @@ import {Button} from '@chakra-ui/react'; import {useState} from 'react'; +import {useParams} from 'react-router-dom'; import styles from './RegionSearch.module.scss'; @@ -12,15 +13,13 @@ import RegionSearchBox from '@/components/TripSpace/RegionSearchBox/RegionSearch import RegionTagItem from '@/components/TripSpace/RegionTagItem/RegionTagItem'; import SelectHeader from '@/components/TripSpace/SelectHeader/SelectHeader'; -import {getSpaceId} from '@/utils/getSpaceId'; - import {Region} from '@/types/regionSearch'; function RegionSearch() { const showToast = CustomToast(); - const spaceId = getSpaceId(); + const {id} = useParams(); const {data: regions} = useGetRegions(); - const {data: spaceData} = useGetSpace(spaceId); + const {data: spaceData} = useGetSpace(Number(id)); const putRegion = usePutRegions(); const prevRegion = spaceData?.data?.city?.split(','); const [isInputFocused, setIsInputFocused] = useState(false); @@ -57,7 +56,7 @@ function RegionSearch() { }; const editRegions = async () => { - await putRegion.mutateAsync({spaceId: spaceId, cities: selectedRegions}); + await putRegion.mutateAsync({spaceId: Number(id), cities: selectedRegions}); }; return ( diff --git a/src/pages/Trip/CheckTrip.tsx b/src/pages/Trip/CheckTrip.tsx new file mode 100644 index 00000000..f854af01 --- /dev/null +++ b/src/pages/Trip/CheckTrip.tsx @@ -0,0 +1,130 @@ +import {Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs} from '@chakra-ui/react'; +import {useEffect} from 'react'; +import {AiOutlineBell as AlarmIcon} from 'react-icons/ai'; +import {AiOutlineMenu as MenuIcon} from 'react-icons/ai'; +import {FiPlus as PlusIcon} from 'react-icons/fi'; +import {useNavigate} from 'react-router-dom'; +import {useSetRecoilState} from 'recoil'; + +import styles from './Trip.module.scss'; + +import {useGetRecentSpace} from '@/hooks/Spaces/space'; +import {usePostSpace} from '@/hooks/Spaces/useSpaces'; +import useGoLogin from '@/hooks/useGoLogin'; + +import AlertModal from '@/components/AlertModal/AlertModal'; + +import {isModalOpenState} from '@/recoil/vote/alertModal'; + +function CheckTrip() { + const {data: recentSpaceData} = useGetRecentSpace(); + const goLogin = useGoLogin(); + const setIsModal = useSetRecoilState(isModalOpenState); + const {mutate} = usePostSpace(); + const navigate = useNavigate(); + + useEffect(() => { + console.log(recentSpaceData); + // 비로그인 유저 + if (recentSpaceData.status === 401) { + goLogin(); + } + + // 유효한 여행 스페이스 없는 유저 + if (recentSpaceData.responseCode === 404) { + setIsModal(true); + } + + // 최근 방문 스페이스 있는 유저 + if (recentSpaceData.data) { + navigate(`/trip/${recentSpaceData.data.id}`, {state: {id: recentSpaceData.data.id}}); + } + }, [recentSpaceData]); + + const makeNewSpace = () => { + setIsModal(false); + mutate(undefined, { + onSuccess: (data) => { + if (data) { + navigate(`/trip/${data.data.id}`, {state: {id: recentSpaceData.data.id}}); + } + }, + }); + }; + + return ( + <> +
    +
    + + +
    + +
    +
    +
    D-day
    +
    여행지를 정해주세요
    +
    + 날짜를 정해주세요 + +
    +
    +
    + +
    +
    + +
    + +
    + + + 투표 + + + 일정 + + + +
    + + + <> + + + <> + + +
    +
    +
    + + + ); +} + +export default CheckTrip; diff --git a/src/pages/Trip/Trip.module.scss b/src/pages/Trip/Trip.module.scss index c37496cd..3b884f4d 100644 --- a/src/pages/Trip/Trip.module.scss +++ b/src/pages/Trip/Trip.module.scss @@ -116,10 +116,6 @@ span { border: 1px solid $neutral0; } - - span:nth-child(1) { - display: none; - } } } diff --git a/src/pages/Trip/Trip.tsx b/src/pages/Trip/Trip.tsx index d2ec8fc0..b2740e33 100644 --- a/src/pages/Trip/Trip.tsx +++ b/src/pages/Trip/Trip.tsx @@ -13,9 +13,12 @@ import {useEffect, useRef, useState} from 'react'; import {AiOutlineBell as AlarmIcon} from 'react-icons/ai'; import {AiOutlineMenu as MenuIcon} from 'react-icons/ai'; import {FiPlus as PlusIcon} from 'react-icons/fi'; +import {useNavigate, useParams} from 'react-router-dom'; import styles from './Trip.module.scss'; +import {useGetJourneys, useGetSpace} from '@/hooks/Spaces/space'; + import Alarm from '@/components/Alarm/Alarm'; import BottomSlide from '@/components/BottomSlide/BottomSlide'; import RouteTabPanel from '@/components/Route/RouteTabPanel/RouteTabPanel'; @@ -25,40 +28,51 @@ import FriendList from '@/components/TripSpace/FriendList/FriendList'; import InviteFriends from '@/components/TripSpace/InviteFriends/InviteFriends'; import VoteTabPanel from '@/components/VoteTabPanel/VoteTabPanel'; +import {checkDDay} from '@/utils/checkDday'; +import {setSpaceDate} from '@/utils/formatDate'; +import {getMapCenter} from '@/utils/getMapCenter'; + import {LatLng} from '@/types/route'; +import {Member} from '@/types/sidebar'; function Trip() { const news = localStorage.getItem('news'); - // 임시 데이터 - const users = [ - {name: '김철수', src: ''}, - {name: '나철수', src: ''}, - {name: '다철수', src: 'https://bit.ly/kent-c-dodds'}, - {name: '라철수', src: 'https://bit.ly/prosper-baba'}, - {name: '마철수', src: 'https://bit.ly/code-beast'}, - ]; - const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const mapRef = useRef(null); - const [center, setCenter] = useState({lat: 37, lng: 131}); // 기준: 독도 - const {isOpen: isBottomSlideOpen, onOpen: onBottomSlideOpen, onClose: onBottomSlideClose} = useDisclosure(); const {isOpen: isSlideBarOpen, onOpen: onSlideBarOpen, onClose: onSlideBarClose} = useDisclosure(); - const {isOpen: isInviteOpen, onOpen: onInviteOpen, onClose: onInviteClose} = useDisclosure(); - const {isOpen: isFriendListOpen, onOpen: onFriendListOpen, onClose: onFriendListClose} = useDisclosure(); - const {isOpen: isAlarmOpen, onOpen: onAlarmOpen, onClose: onAlarmClose} = useDisclosure(); + const {id} = useParams(); + const {data: spaceData} = useGetSpace(Number(id)); + const {data: journeysData} = useGetJourneys(Number(id)); + const mapRef = useRef(null); + const [center, setCenter] = useState(getMapCenter(journeysData.data)); + const navigate = useNavigate(); + const users = spaceData?.data?.members; + + useEffect(() => { + console.log('아임센터', center); + }, [center]); + + if (!spaceData.data) { + console.log('로그인 안 했음'); + navigate('/trip'); + } + + if (spaceData.data) { + console.log(spaceData.data); + console.log(journeysData.data); + } useEffect(() => { const map = mapRef.current; if (map) { map.relayout(); - setCenter({lat: 37.76437082535426, lng: 128.87675285339355}); + setCenter(getMapCenter(journeysData.data)); } - }, [selectedTabIndex]); + }, [selectedTabIndex, journeysData]); return ( <> @@ -75,10 +89,19 @@ function Trip() {
    -
    D-day
    -
    여행지를 정해주세요
    +
    {checkDDay(spaceData.data.dueDate)}
    +
    + {spaceData.data.city ? spaceData.data.city : '여행지를 정해주세요'} +
    - 날짜를 정해주세요 + + {spaceData.data.endDate + ? setSpaceDate( + spaceData.data.startDate, + spaceData.data.startDate === spaceData.data.endDate ? '' : spaceData.data.endDate, + ) + : '날짜를 정해주세요'} + @@ -86,12 +109,19 @@ function Trip() {
    } /> } /> - } /> + } />
    diff --git a/src/pages/User/MyReview/MyReview.module.scss b/src/pages/User/MyReview/MyReview.module.scss index c5b8b94f..155aa94a 100644 --- a/src/pages/User/MyReview/MyReview.module.scss +++ b/src/pages/User/MyReview/MyReview.module.scss @@ -1,4 +1,4 @@ -@use "@/sass" as *; +@use '@/sass' as *; .container { margin-top: 80px; @@ -29,6 +29,12 @@ background-position: center; background-size: cover; + background-repeat: no-repeat; + + &.default { + border: 1px solid $neutral200; + background-size: 60% 60%; + } } &__text { diff --git a/src/pages/User/MyReview/MyReview.tsx b/src/pages/User/MyReview/MyReview.tsx index 79552f2c..701d7c26 100644 --- a/src/pages/User/MyReview/MyReview.tsx +++ b/src/pages/User/MyReview/MyReview.tsx @@ -1,194 +1,83 @@ -import { useDisclosure } from "@chakra-ui/react"; -import { useRef } from "react"; - -import styles from "./MyReview.module.scss"; - -import Header from "@/components/Auth/Header/Header"; -import BottomSlide from "@/components/BottomSlide/BottomSlide"; -import ReviewImageSlider from "@/components/Detail/Contents/Review/ReviewImageSlider/ReviewImageSlider"; -import ActionList from "@/components/MyReview/ActionList/ActionList"; - -import Meatball from "@/assets/icons/meatball.svg?react"; -import Star from "@/assets/icons/star_fill.svg?react"; -import { setMyReviewDate } from "@/utils/formatDate"; - -const data = [ - { - id: 1231, - place: { - id: 41414, - area: "대전", - category: "맛집 · 서울", - title: "대전 성심당", - thumbnail: - "https://cdn.safetimes.co.kr/news/photo/202210/115164_98919_3327.jpg", - }, - visitedAt: "2023-11-18", - rating: "5.0", - content: - "아주 좋아요. 자주 다니고 있어요. 친구들이랑 저녁에 운동하기 좋아요. 다음에 또 가고 싶네요", - images: [ - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - ], - }, - { - id: 12555, - place: { - id: 42311, - area: "대전", - category: "맛집 · 서울", - title: "대전 성심당", - thumbnail: - "https://cdn.safetimes.co.kr/news/photo/202210/115164_98919_3327.jpg", - }, - visitedAt: "2023-11-18", - rating: "5.0", - content: - "아주 좋아요. 자주 다니고 있어요. 친구들이랑 저녁에 운동하기 좋아요. 다음에 또 가고 싶네요", - images: [ - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - ], - }, - { - id: 1111, - place: { - id: 535334, - area: "대전", - category: "맛집 · 서울", - title: "대전 성심당", - thumbnail: - "https://cdn.safetimes.co.kr/news/photo/202210/115164_98919_3327.jpg", - }, - visitedAt: "2023-11-18", - rating: "5.0", - content: - "아주 좋아요. 자주 다니고 있어요. 친구들이랑 저녁에 운동하기 좋아요. 다음에 또 가고 싶네요", - images: [ - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - ], - }, - { - id: 221411, - place: { - id: 655454, - area: "대전", - category: "맛집 · 서울", - title: "대전 성심당", - thumbnail: - "https://cdn.safetimes.co.kr/news/photo/202210/115164_98919_3327.jpg", - }, - visitedAt: "2023-11-18", - rating: "5.0", - content: - "아주 좋아요. 자주 다니고 있어요. 친구들이랑 저녁에 운동하기 좋아요. 다음에 또 가고 싶네요", - images: [ - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - ], - }, - { - id: 221331, - place: { - id: 645445, - area: "대전", - category: "맛집 · 서울", - title: "대전 성심당", - thumbnail: - "https://cdn.safetimes.co.kr/news/photo/202210/115164_98919_3327.jpg", - }, - visitedAt: "2023-11-18", - rating: "5.0", - content: - "아주 좋아요. 자주 다니고 있어요. 친구들이랑 저녁에 운동하기 좋아요. 다음에 또 가고 싶네요", - images: [ - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - "https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/202301/02/8c47d46b-ef7f-4508-b451-b0ad96c9b672.jpg", - ], - }, -]; +import {useDisclosure} from '@chakra-ui/react'; +import {useRef} from 'react'; + +import styles from './MyReview.module.scss'; + +import {useInfiniteScroll} from '@/hooks/useInfiniteScroll'; +import {useGetMyReview} from '@/hooks/User/useMyReview'; + +import Header from '@/components/Auth/Header/Header'; +import BottomSlide from '@/components/BottomSlide/BottomSlide'; +import ReviewImageSlider from '@/components/Detail/Contents/Review/ReviewImageSlider/ReviewImageSlider'; +import ActionList from '@/components/MyReview/ActionList/ActionList'; +import ObserveTarget from '@/components/Route/ObserveTarget/ObserveTarget'; + +import defaultImage from '@/assets/icons/city_default.svg'; +import Meatball from '@/assets/icons/meatball.svg?react'; +import Star from '@/assets/icons/star_fill.svg?react'; +import {setMyReviewDate} from '@/utils/formatDate'; + +import {Reviews} from '@/types/myReview'; function MyReview() { - const { - isOpen: isBottomSlideOpen, - onOpen: onBottomSlideOpen, - onClose: onBottomSlideClose, - } = useDisclosure(); + const {isOpen: isBottomSlideOpen, onOpen: onBottomSlideOpen, onClose: onBottomSlideClose} = useDisclosure(); + const [reviews, hasNextData, inViewRef] = useInfiniteScroll(useGetMyReview); const clickedReviewId = useRef(); return (
    -
    +
      - {data.map(({ id, place, visitedAt, rating, content, images }) => ( -
    • -
      -
      -
      - -
      -
      - {place.title} -
      -
      - {place.category} + {reviews?.pages.map((page) => + page.data.reviews.map(({id, place, visitedAt, rating, content, images}: Reviews) => ( +
    • +
      +
      +
      + +
      +
      {place.title}
      +
      {place.category}
      -
      - -
    - - -
    - +
    -
    {rating}
    -
    {`${setMyReviewDate(visitedAt)} 방문`}
    -
    + +
    + +
    +
    {rating}
    -

    {content}

    +
    {`${setMyReviewDate(visitedAt)} 방문`}
    +
    - {images && } - - ))} +

    {content}

    + + {images && } + + )), + )} + {hasNextData && } + (); + const [filterData, setFilterData] = useState(); + const [categoryChange, setCategoryChange] = useState(false); + const [searchParams] = useSearchParams(); + const [filter, setFilter] = useState({ + category: 0, + location: '전국', + placeID: 'undefined', + tripDate: 'undefinde', + sort: '등록순', + }); + + useEffect(() => { + const querystring = { + category: searchParams.get('category'), + location: searchParams.get('location'), + placeID: searchParams.get('placeID'), + tripDate: searchParams.get('tripDate'), + sort: searchParams.get('sort'), + }; + if ( + querystring.category && + querystring.placeID && + querystring.location && + querystring.sort && + !querystring.tripDate + ) { + setFilter({ + category: parseInt(querystring.category), + location: querystring.location, + sort: querystring.sort, + placeID: querystring.placeID, + tripDate: filter.tripDate, + }); + } + if ( + querystring.category && + querystring.placeID && + querystring.location && + querystring.sort && + querystring.tripDate + ) { + setFilter({ + category: parseInt(querystring.category), + location: querystring.location, + sort: querystring.sort, + placeID: querystring.placeID, + tripDate: querystring.tripDate, + }); + } + }, [searchParams]); + + useEffect(() => { + getUserWishes(setData); + }, []); + + useEffect(() => { + if (data) { + if (filter.category !== 0) { + let filterData: SearchItemType[]; + if (filter.category === 14) { + filterData = data.places.filter((data) => data.contentTypeId === 14 || data.contentTypeId === 15); + } else { + filterData = data.places.filter((data) => data.contentTypeId === filter.category); + } + setFilterData(filterData); + } else { + setFilterData(data.places); + } + } + }, [data, filter.category]); + + return ( +
    + {filter.placeID === 'undefined' ? ( +

    + 찜 목록 +

    + ) : ( + + )} + {data && } +
    + + +
    +
      + {filterData && filterData?.length > 0 ? ( + filterData.map((data) => ) + ) : ( +
      + + 찜 목록이 비었습니다. +
      + )} +
    + {filter.placeID !== 'undefined' && } +
    + ); +} + +export default Wishes; diff --git a/src/routes/MainRouter/MainRouter.tsx b/src/routes/MainRouter/MainRouter.tsx index d2f6db3f..3da08744 100644 --- a/src/routes/MainRouter/MainRouter.tsx +++ b/src/routes/MainRouter/MainRouter.tsx @@ -1,6 +1,6 @@ -import {useEffect, useState} from 'react'; -import {Helmet} from 'react-helmet-async'; -import {Route, Routes, useLocation} from 'react-router-dom'; +import {Route, Routes} from 'react-router-dom'; + +import Head from '@/components/Head/Head'; import AddPlaceFromVote from '@/pages/AddPlaceFromVote/AddPlaceFromVote'; import FindPassword from '@/pages/Auth/FindPassword/FindPassword'; @@ -17,6 +17,7 @@ import Home from '@/pages/Home/Home'; import MapZoomIn from '@/pages/MapZoomIn/MapZoomIn'; import RegionSearch from '@/pages/RegionSearch/RegionSearch'; import SearchFromHome from '@/pages/SearchFromHome/SearchFromHome'; +import CheckTrip from '@/pages/Trip/CheckTrip'; import Trip from '@/pages/Trip/Trip'; import ModifyProfile from '@/pages/User/ModifyProfile/ModifyProfile'; import MyReview from '@/pages/User/MyReview/MyReview'; @@ -25,50 +26,249 @@ import User from '@/pages/User/User'; import UserPrivacy from '@/pages/User/UserPrivacy/UserPrivacy'; import Vote from '@/pages/Vote/Vote'; import VoteMemo from '@/pages/Vote/VoteMemo/VoteMemo'; +import Wishes from '@/pages/Wishes/Wishes'; import Dashboard from '@/routes/Dashboard/Dashboard'; -import {getTitle} from '@/utils/getTitle'; function MainRouter() { - const [title, setTitle] = useState(''); - const location = useLocation(); - useEffect(() => { - setTitle(getTitle(location.pathname)); - }, [location]); - return ( - <> - - TRIPVOTE | {title} - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + }> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + ); } diff --git a/src/types/modal.ts b/src/types/modal.ts index c897fcce..1015ec04 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -4,4 +4,10 @@ export interface ModalProps { onClose: () => void; } -export type LeaveTripModalProps = Omit; +export type LeaveTripModalProps = Omit; + +export interface DeletePlacesModalProps { + isOpen: boolean; + onClose: () => void; + placeList: string[]; +} diff --git a/src/types/myReview.ts b/src/types/myReview.ts index 47245cdb..ff1b6a78 100644 --- a/src/types/myReview.ts +++ b/src/types/myReview.ts @@ -1,5 +1,22 @@ -import { MutableRefObject } from "react"; +import {MutableRefObject} from 'react'; export interface ActionListProps { reviewId: MutableRefObject; } + +export interface Reviews { + content: string; + id: number; + images: string[]; + place: Place; + rating: number; + visitedAt: string; +} + +export interface Place { + area: string; + category: string; + id: number; + thumbnail: string; + title: string; +} diff --git a/src/types/route.ts b/src/types/route.ts index bf04d3b3..7f11f9a4 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -2,11 +2,13 @@ import {Dispatch} from 'react'; import {SwiperRef} from 'swiper/react'; export interface DraggablePlaceCardProps { - id: number; + selectedId: number; order: number; name: string; category: string; address: string; + placeId: number; + contentTypeId: number; editMode: boolean; selectedPlaces: string[]; onSelect: (name: string) => void; @@ -17,13 +19,12 @@ export interface DraggablePlaceCardProps { export type PlaceCardProps = Pick; export interface PlaceOrder { - id: number; - Order: number; + selectedId: number; + order: number; place: Place; } export interface Place { - id: number; title: string; thumbnail: string; address: string; @@ -31,27 +32,8 @@ export interface Place { latitude: number; longitude: number; category: string; -} - -export interface Journey { - id: number; - date: string; - places: PlaceOrder[]; -} - -export interface PlaceList { - id: number; - Order: number; - place: { - id: number; - title: string; - thumbnail: string; - address: string; - addressDetail: string; - latitude: number; - longitude: number; - category: string; - }; + contentTypeId: number; + placeId: number; } export interface DayRouteProps { @@ -97,10 +79,53 @@ export interface LatLng { export interface MapInTripProps { mapRef: React.RefObject; center: LatLng; + journeysData: Journeys; +} + +export interface Journey { + journeyId: number; + date: string; + places: PlaceList[]; +} + +export interface PlaceList { + selectedId: number; + order: number; + place: { + title: string; + thumbnail: string; + address: string; + addressDetail: string; + latitude: number; + longitude: number; + category: string; + contentTypeId: number; + placeId: number; + }; } export interface Journeys { journeys: Journey[]; + // journeys: { + // journeyId: number; + // date: number; + // places: [ + // { + // selectedId: number; + // order: 0; + // place: { + // placeId: number; + // title: string; + // thumbnail: string; + // address: string; + // addressDetail: string; + // latitude: number; + // longitude: number; + // category: string; + // }; + // }, + // ]; + // }; } export interface RouteMapSlideProps { @@ -132,6 +157,7 @@ export interface SpaceResponse { title: string; startDate: string | null; endDate: string | null; + dueDate: number; city: string | null; thumbnail: string | null; members: [ @@ -142,6 +168,7 @@ export interface SpaceResponse { }, ]; }; + status: number; responseCode: number; detail?: string; } @@ -156,3 +183,33 @@ export interface SpaceDateParams { startDate: string; endDate: string; } + +export interface Member { + id: number; + nickname: string; + profile: string; +} + +export interface Members { + members: Member[]; +} + +export interface ExitSpaceParams { + spaceId: number; +} + +export interface PlaceParams { + spaceId: number; + journeyId: number; + placeIds: number[]; +} + +export interface SelectPlace { + journeyId: number; + placeIds: number[]; +} + +export interface journeyParams { + spaceId: number; + places: SelectPlace[]; +} diff --git a/src/types/user.ts b/src/types/user.ts index a0621cda..47a3c0fc 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -28,6 +28,5 @@ export interface TabProps { } export interface MySpaceListProps { - data: MySpaces | null; tab: string; } diff --git a/src/types/wish.ts b/src/types/wish.ts new file mode 100644 index 00000000..9025c1a8 --- /dev/null +++ b/src/types/wish.ts @@ -0,0 +1,28 @@ +import {SearchItemType} from './home'; + +export interface WishesData { + id: number; + area: string; + category: string; + title: string; + thumbnail: string; + contentTypeId: number; +} + +export interface Wishes { + places: SearchItemType[]; + pageNumber: number; + pageSize: number; + totalPages: number; + totalResult: number; + first: boolean; + last: boolean; +} + +export interface WishFilterType { + category: number; + location: string; + placeID: string; + tripDate: string; + sort: string; +} diff --git a/src/utils/checkDday.ts b/src/utils/checkDday.ts new file mode 100644 index 00000000..9dc1b49c --- /dev/null +++ b/src/utils/checkDday.ts @@ -0,0 +1,6 @@ +// ex) 2024년 6월 +export const checkDDay = (dueDate: number) => { + if (dueDate === 0) return `D-day`; + else if (dueDate > 0) return `D-${dueDate}`; + else return '지난 여행'; +}; diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts index 3a01205c..1a23e87e 100644 --- a/src/utils/formatDate.ts +++ b/src/utils/formatDate.ts @@ -1,6 +1,6 @@ import {format} from 'date-fns'; -const createDate = (date: string) => { +export const createDate = (date: string) => { const arrayForm = date.split('-').map((el) => Number(el)); return new Date(arrayForm[0], arrayForm[1] - 1, arrayForm[2]); @@ -63,3 +63,9 @@ export const setMyReviewDate = (visitedAt: string) => { const visitFormat = format(createDate(visitedAt), "yyyy'년' M'월'"); return visitFormat; }; + +// ex) 2024.01.12(월) +export const setRouteDate = (date: string) => { + const dateFormat = format(createDate(date), 'yyyy.MM.dd(EEEEEE)'); + return changeDOWFormat(dateFormat); +}; diff --git a/src/utils/getMapCenter.ts b/src/utils/getMapCenter.ts new file mode 100644 index 00000000..c58958de --- /dev/null +++ b/src/utils/getMapCenter.ts @@ -0,0 +1,11 @@ +import {Journeys} from '@/types/route'; + +export const getMapCenter = (journeysData: Journeys) => { + if (journeysData.journeys.length > 0 && journeysData.journeys[0].places.length > 0) { + const lat = journeysData.journeys[0].places[0].place.latitude; + const lng = journeysData.journeys[0].places[0].place.longitude; + return {lat: lat, lng: lng}; + } else { + return {lat: 37, lng: 131}; // 기준 독도 + } +}; diff --git a/src/utils/getTitle.ts b/src/utils/getTitle.ts deleted file mode 100644 index b3324315..00000000 --- a/src/utils/getTitle.ts +++ /dev/null @@ -1,48 +0,0 @@ -export const getTitle = (pathname: string) => { - if (pathname.startsWith('/trip/') && /\d+$/.test(pathname)) { - return '여행스페이스'; - } - if (pathname.startsWith('/votes/') && pathname.endsWith('/map')) { - return '지도'; - } - if (pathname.startsWith('/votes/') && pathname.endsWith('/votememo')) { - return '메모'; - } - if (pathname.startsWith('/votes/') && /\d+$/.test(pathname)) { - return '투표'; - } - if (pathname.startsWith('/detail/')) { - return '상세'; - } - if (pathname.startsWith('/trip/') && pathname.includes('/selectDate')) { - return '날짜선택'; - } - if (pathname.startsWith('/trip/') && pathname.includes('/selectRegion')) { - return '지역선택'; - } - if (pathname.startsWith('/trip/') && pathname.includes('/map')) { - return '지도'; - } - if (pathname.startsWith('/trip/') && pathname.includes('/add/vote')) { - return '일정추가'; - } - const pathMap: {[key: string]: string} = { - '/': '홈', - '/heart': '찜', - '/user': '마이페이지', - '/user/privacy': '계정관리', - '/user/profile/edit': '프로필 편집', - '/user/myspace': '내 여행스페이스', - '/user/myreview': '내 리뷰', - '/home/search': '검색', - '/auth/login': '로그인', - '/auth/signup': '회원가입', - '/auth/signup/agreePrivacy': '개인정보이용 약관', - '/auth/signup/agreeService': '서비스이용 약관', - '/auth/password/find': '비밀번호 찾기', - '/auth/password/modify': '비밀번호 수정', - '/auth/withdrawal': '다음에 또 만나요', - }; - - return pathMap[pathname] || '알 수 없는 경로'; -}; diff --git a/src/utils/optimizePlace.ts b/src/utils/optimizePlace.ts index 289a315b..e90d9cb0 100644 --- a/src/utils/optimizePlace.ts +++ b/src/utils/optimizePlace.ts @@ -1,60 +1,26 @@ -interface PlaceDetail { - id: number; - title: string; - thumbnail: string; - address: string; - addressDetail: string; - latitude: number; - longitude: number; - category: string; +import {PlaceList} from '@/types/route'; +function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { + const dx = lat1 - lat2; + const dy = lon1 - lon2; + return Math.sqrt(dx * dx + dy * dy); } - -interface Place { - id: number; - Order: number; - place: PlaceDetail; -} - -function calculateDistance(x1: number, y1: number, x2: number, y2: number): number { - return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); -} - -export function findShortestPath(places: Place[]): Place[] { - const startPoint: Place = places[0]; - const endPoint: Place = places +export function findShortestPath(places: PlaceList[]): PlaceList[] { + if (places.length === 0) return []; + const start = places[0]; + let end = places .slice() .reverse() - .find((place) => place.place.category === '숙소') as Place; - - let waypoints: Place[] = places.slice(1, places.length); - waypoints = waypoints.filter((place) => place.id !== endPoint.id); - - const sortedPlaces: Place[] = [startPoint]; - let currentPlaceDetail: PlaceDetail = startPoint.place; - - while (waypoints.length > 0) { - waypoints.sort((a, b) => { - const distA: number = calculateDistance( - currentPlaceDetail.latitude, - currentPlaceDetail.longitude, - a.place.latitude, - a.place.longitude, - ); - const distB: number = calculateDistance( - currentPlaceDetail.latitude, - currentPlaceDetail.longitude, - b.place.latitude, - b.place.longitude, - ); - return distA - distB; - }); - - const nextPlace = waypoints.shift() as Place; - sortedPlaces.push(nextPlace); - currentPlaceDetail = nextPlace.place; - } - - sortedPlaces.push(endPoint); - - return sortedPlaces; + .find((p) => p.place.category === '숙소'); + if (!end) end = places[places.length - 1]; + const waypoints = places.slice(1, places.length - 1).filter((p) => p !== end); + const waypointsWithDistance = waypoints.map((p) => ({ + ...p, + distance: calculateDistance(start.place.latitude, start.place.longitude, p.place.latitude, p.place.longitude), + })); + waypointsWithDistance.sort((a, b) => a.distance - b.distance); + return [ + start, + ...waypointsWithDistance.map((p) => ({selectedId: p.selectedId, order: p.order, place: p.place})), + end, + ].filter(Boolean); }