diff --git a/package.json b/package.json index 2a63f1a5..10b3be84 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle-group": "^1.0.4", "@stomp/stompjs": "^7.0.0", "@svgr/rollup": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 753ad932..0720f243 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toast': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toggle-group': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) @@ -2430,6 +2433,38 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-toast@1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.45)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.45)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.45)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.45)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.45)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.45 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} peerDependencies: diff --git a/src/@types/service.ts b/src/@types/service.ts index 4ea36ae0..bae375b2 100644 --- a/src/@types/service.ts +++ b/src/@types/service.ts @@ -2,7 +2,7 @@ export type subInfoRes = { status: number; message: string; data: { - tripId: number; + tripId: string; startDate: string; endDate: string; numberOfPeople: number; @@ -16,7 +16,7 @@ export type subItemRes = { status: number; message: string; data: { - tripId: number; + tripId: string; visitDate: string; transportation: 'CAR' | 'PUBLIC_TRANSPORTATION'; tripItems: { @@ -52,7 +52,7 @@ export type subPathRes = { status: number; message: string; data: { - tripId: number; + tripId: string; visitDate: string; transportation: 'CAR' | 'PUBLIC_TRANSPORTATION'; paths: { @@ -77,7 +77,7 @@ export type subMemberRes = { status: number; message: string; data: { - tripId: number; + tripId: string; tripMembers: { memberId: number; name: string; @@ -91,7 +91,7 @@ export type subBudgetRes = { status: number; message: string; data: { - tripId: number; + tripId: string; budget: number; calculatedPrice: number; } | null; @@ -103,6 +103,7 @@ export type SocketContextType = { tripPath: subPathRes | null; tripMember: subMemberRes | null; tripBudget: subBudgetRes | null; + tripId: string; callBackPub: (callback: () => void) => void; }; diff --git a/src/@types/socket.types.ts b/src/@types/socket.types.ts index 2a70ed7b..49244096 100644 --- a/src/@types/socket.types.ts +++ b/src/@types/socket.types.ts @@ -2,7 +2,7 @@ type subInfoMessage = (message: { status: number; message: string; data: { - tripId: number; + tripId: string; startDate: string; endDate: string; numberOfPeople: number; @@ -16,7 +16,7 @@ type subItemMessage = (response: { status: number; message: string; data: { - tripId: number; + tripId: string; visitDate: string; transportation: 'CAR' | 'PUBLIC_TRANSPORTATION'; tripItems: { @@ -36,7 +36,7 @@ type subPathMessage = (response: { status: number; message: string; data: { - tripId: number; + tripId: string; visitDate: string; transportation: 'CAR' | 'PUBLIC_TRANSPORTATION'; paths: { @@ -61,7 +61,7 @@ type subMemberMessage = (response: { status: number; message: string; data: { - tripId: number; + tripId: string; tripMembers: { memberId: number; name: string; @@ -76,7 +76,7 @@ type subBudgetMessage = (response: { status: number; message: string; data: { - tripId: number; + tripId: string; budget: number; calculatedPrice: number; }; @@ -98,7 +98,7 @@ interface pubAddTripItem { } interface pubUpdatePrice { - tripId: number; + tripId: string; visitDate: string; price: number; } diff --git a/src/@types/tours.types.ts b/src/@types/tours.types.ts index 2b722c6d..ccd95671 100644 --- a/src/@types/tours.types.ts +++ b/src/@types/tours.types.ts @@ -37,3 +37,18 @@ export interface TourType { longitude?: string; latitude?: string; } + +export interface LikedListType { + tripLikedItemId: number; + tourItemId: number; + contentTypeId: number; + title: string; + ratingAverage: number; + reviewCount: number; + smallThumbnailUrl: string; + tourAddress: string; + prefer: boolean; + notPrefer: boolean; + preferTotalCount: number; + notPreferTotalCount: number; +} diff --git a/src/@types/trips.types.ts b/src/@types/trips.types.ts index 3197ccfe..6b6bceec 100644 --- a/src/@types/trips.types.ts +++ b/src/@types/trips.types.ts @@ -3,12 +3,12 @@ interface TripRequest { numberOfPeople: number; startDate: string | null; endDate: string | null; - area: string | null; - subarea: string | null; + area?: string | null; + subarea?: string | null; } interface MyTripType { - tripId: number; + tripId: string; tripName: string; startDate: string; endDate: string; @@ -35,18 +35,18 @@ interface ourTripType { } interface ThumbsProps { - tripId: number; + tripId: string; tourId: number; prefer: boolean; notPrefer: boolean; } - + interface AuthorityType { status: number; message: string; data: { memberId: number; tripAuthority: string; - TripId: number; + tripId: string; }; } diff --git a/src/api/socket.ts b/src/api/socket.ts index 000830cf..0ade3027 100644 --- a/src/api/socket.ts +++ b/src/api/socket.ts @@ -101,8 +101,6 @@ export const pubUpdateTripItem = ( destination: `/pub/trips/${tripId}/updateTripItemOrder`, body: JSON.stringify(pubUpdateTripItem), }); - - console.log('데이터', pubUpdateTripItem); }; // 여행 날짜별 교통 수단 변경 이벤트 발생시 (01/16 업데이트) @@ -136,7 +134,6 @@ export const pubDeleteItem = ( destination: `/pub/tripItems/${tripItemId}/deleteItem`, body: JSON.stringify(pubDeleteItem), }); - console.log(pubDeleteItem); }; // 멤버 여정 페이지로 입장 이벤트 발생시 diff --git a/src/api/trips.ts b/src/api/trips.ts index d50faa39..0a89bb54 100644 --- a/src/api/trips.ts +++ b/src/api/trips.ts @@ -4,19 +4,36 @@ import authClient from './authClient'; // 여정 관련 API // 여정 상세조회 -export const getTrips = async (tripId: number) => { +export const getTrips = async (tripId: string) => { const res = await client.get(`trips/${tripId}`); return res; }; // 여정 기본정보 수정 -export const putTrips = async (tripId: number, tripsData: TripRequest) => { - const res = await client.put(`trips/${tripId}`, tripsData); +export const putTrips = async ( + tripId: string, + tripRequestData: TripRequest, +) => { + const res = await authClient.put(`trips/${tripId}`, tripRequestData); + return res; +}; + +// 여행 상세페이지에서 여정에 여행지 등록 +export const postTripsItem = async ( + tripId: string, + tourItemId: number, + visitDate: string, +) => { + const requestBody = { + tourItemId: tourItemId, + visitDate: visitDate, + }; + const res = await authClient.post(`trips/${tripId}`, requestBody); return res; }; // 여정 탈퇴 -export const deleteTrips = async (tripId: number) => { +export const deleteTrips = async (tripId: string) => { try { const res = await authClient.delete(`trips/${tripId}`); @@ -34,7 +51,7 @@ export const postTrips = async (tripsData: TripRequest) => { // 우리의 관심목록 조회 export const getTripsLike = async ( - tripId: number, + tripId: string, page: number, size: number, ) => { @@ -46,7 +63,7 @@ export const getTripsLike = async ( }; // 우리의 관심 목록 등록 -export const postTripsLike = async (tripId: number, tourItemIds: number[]) => { +export const postTripsLike = async (tripId: string, tourItemIds: number[]) => { const requestBody = { tourItemIds: tourItemIds, }; @@ -59,7 +76,7 @@ export const postTripsLike = async (tripId: number, tourItemIds: number[]) => { // 우리의 관심 목록 좋아요/싫어요 export const postTripsLikeHate = async ( - tripId: number, + tripId: string, tourId: number, prefer: boolean, notPrefer: boolean, @@ -71,17 +88,17 @@ export const postTripsLikeHate = async ( }; // 우리의 여행취향 조회 -export const getTripsSurvey = async (tripId: number) => { +export const getTripsSurvey = async (tripId: string) => { const res = await client.get(`trips/${tripId}/survey`); return res; }; // 우리의 여행취향 참여/미참여 회원 조회 -export const getTripsSurveyMembers = async (tripId: number) => { +export const getTripsSurveyMembers = async (tripId: string) => { const res = await client.get(`trips/${tripId}/survey/members`); return res; }; // 여정을 공유하고 있는 회원 조회 -export const getTripsMembers = async (tripId: number) => { +export const getTripsMembers = async (tripId: string) => { const res = await client.get(`trips/${tripId}/members`); return res; }; @@ -91,3 +108,21 @@ export const getTripsAuthority = async (tripId: string) => { const res = await authClient.get(`trips/${tripId}/authority`); return res; }; + +// 나의 여정목록 조회 +export const getMyTrips = async () => { + const res = await authClient.get(`trips`); + return res; +}; + +// 여정 참여 코드 조회 +export const getTripsjoin = async (tripId: string) => { + const res = await authClient.get(`trips/${tripId}/join`); + return res; +}; + +// 여정 참여 +export const postTripsjoin = async (tripId: string, joinCode: string) => { + const res = await authClient.post(`trips/${tripId}/join`, { joinCode }); + return res; +}; diff --git a/src/components/Auth/Login/LoginForm.tsx b/src/components/Auth/Login/LoginForm.tsx index db12f7bc..0491d686 100644 --- a/src/components/Auth/Login/LoginForm.tsx +++ b/src/components/Auth/Login/LoginForm.tsx @@ -1,7 +1,7 @@ import type { AuthRequest } from '@/@types/auth.types'; import { SubmitHandler, useForm } from 'react-hook-form'; import { postEmailLogin } from '@api/auth'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { AxiosError } from 'axios'; import { useState } from 'react'; import SubmitBtn from '@components/common/button/SubmitBtn'; @@ -26,6 +26,8 @@ const LoginForm = () => { }); const navigate = useNavigate(); + const { state } = useLocation(); + // const [userInfo, setUserInfo] = useRecoilState(UserInfoState); const onLoginSubmit: SubmitHandler = async (data) => { @@ -38,7 +40,11 @@ const LoginForm = () => { if (res.data.status === 200) { setItem('accessToken', res.data.data.tokenInfo.accessToken); // setUserInfo(res.data.data.memberDto); - navigate('/'); + if (state) { + navigate(state.prevPath); + } else { + navigate('/'); + } } } catch (err) { if (err instanceof AxiosError) { diff --git a/src/components/DetailSectionBottom/DetailReviewStats.tsx b/src/components/DetailSectionBottom/DetailReviewStats.tsx index c428443c..f87f181b 100644 --- a/src/components/DetailSectionBottom/DetailReviewStats.tsx +++ b/src/components/DetailSectionBottom/DetailReviewStats.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useGetToursReviews } from '@hooks/useReviewStats'; import { v4 as uuidv4 } from 'uuid'; -import { DownIcon } from '@components/common/icons/Icons'; +import { DropdownIcon } from '@components/common/icons/Icons'; import useReviewStatsCalculator from '@hooks/useReviewStatsCalculator'; import { getEmoji } from '@utils/utils'; @@ -45,11 +45,11 @@ const DetailReviewStats = () => {
setShowAll(!showAll)} - className="cursor-pointer transition-transform duration-300" + className="mt-[9px] cursor-pointer transition-transform duration-300" style={{ transform: showAll ? 'rotate(180deg)' : 'rotate(0deg)', }}> - +
)} diff --git a/src/components/DetailSectionTop/DetailAddSchedule.tsx b/src/components/DetailSectionTop/DetailAddSchedule.tsx index d6fac6dd..57880903 100644 --- a/src/components/DetailSectionTop/DetailAddSchedule.tsx +++ b/src/components/DetailSectionTop/DetailAddSchedule.tsx @@ -2,14 +2,31 @@ import * as Dialog from '@radix-ui/react-dialog'; import { CalendarIcon } from '@components/common/icons/Icons'; import Alert from '@components/common/alert/Alert'; import { useNavigate } from 'react-router-dom'; +import { PlusIcon } from '@components/common/icons/Icons'; +import { useGetMyTrips } from '@hooks/useGetMyTrips'; +import { calculateTripDuration } from '@utils/calculateTripDuration'; +import { calculateDayAndDate } from '@utils/utils'; const DetailAddSchedule = () => { + const { myTrips } = useGetMyTrips(); + + const { SmallDayArr } = calculateDayAndDate( + myTrips[0]?.startDate, + myTrips[0]?.endDate, + ); + + console.log(SmallDayArr); + const navigate = useNavigate(); const handleConfirm = () => { navigate('/login'); }; + const handleCreate = () => { + navigate('/create'); + }; + return ( @@ -24,30 +41,51 @@ const DetailAddSchedule = () => { - -
-
-
- -
-
- 강릉 속초 여행 -
-
- 2023.12.20 - 12.22 (3박 4일) + {myTrips.map((trip, index) => { + // 각 여행에 대한 기간을 계산합니다. + const tripDuration = calculateTripDuration( + trip.startDate, + trip.endDate, + ); + + return ( +
+
+
+ {`Thumbnail +
+
+ {trip.tripName} +
+
+ {trip.startDate?.replace(/-/g, '.')} -{' '} + {trip.endDate?.substring(5).replace(/-/g, '.')} ( + {tripDuration}) +
+
-
-
+ ); + })} +
@@ -67,7 +105,7 @@ const DetailAddSchedule = () => { } onConfirm={handleConfirm}>
-
diff --git a/src/components/MyTrip/MyTripItem.tsx b/src/components/MyTrip/MyTripItem.tsx index 2762a3d1..f327aa9c 100644 --- a/src/components/MyTrip/MyTripItem.tsx +++ b/src/components/MyTrip/MyTripItem.tsx @@ -31,7 +31,7 @@ const MyTripItem: React.FC = ({ myTripList }) => { const tripDuration = calculateTripDuration(startDate, endDate); const { mutate: deleteMyTripMutate } = useMutation({ - mutationFn: (tripId: number) => deleteTrips(tripId), + mutationFn: (tripId: string) => deleteTrips(tripId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['myTrips'] }); }, diff --git a/src/components/Plan/PlanEditItemBox.tsx b/src/components/Plan/PlanEditItemBox.tsx index e5b0f944..4c5ce80d 100644 --- a/src/components/Plan/PlanEditItemBox.tsx +++ b/src/components/Plan/PlanEditItemBox.tsx @@ -14,6 +14,8 @@ import { pubUpdateTripItemReq } from '@/@types/service'; import Alert from '@components/common/alert/Alert'; import ToastPopUp from '@components/common/toastpopup/ToastPopUp'; import PlanMoveItem from './PlanMoveItem'; +import { useRecoilState } from 'recoil'; +import { isEditState } from '@recoil/socket'; type PlanItemBoxProps = { item: TripItem[]; @@ -33,7 +35,7 @@ const PlanEditItemBox = ({ } const { callBackPub } = useContext(socketContext); - + const [, setIsEdit] = useRecoilState(isEditState); const [items, setItems] = useState(item); const [newData, setNewData] = useState(null); const [selectedItemId, setSelectedItemId] = useState(null); @@ -78,6 +80,7 @@ const PlanEditItemBox = ({ noun: '여행지', verb: '삭제', })); + setIsEdit(false); }; const handleRadioChange = (id: number | null) => { @@ -105,7 +108,10 @@ const PlanEditItemBox = ({ {(provided) => ( -
+
{day}
{items.map((item, index) => ( = ({ date, day }) => { const navigate = useNavigate(); const { tripAuthority } = useGetTripsAuthority(); - const [isEdit, SetIsEdit] = useState(false); - const tripId = useRecoilValue(tripIdState); + const [isEdit, setIsEdit] = useRecoilState(isEditState); + const tap = useRecoilValue(tapState); - const [visitDate, setVisitDate] = useRecoilState(visitDateState); - const { tripItem, tripPath, callBackPub } = useContext(socketContext); - console.log(visitDate); + const [, setVisitDate] = useRecoilState(visitDateState); + const { tripItem, tripPath, callBackPub, tripId } = useContext(socketContext); useEffect(() => { if (tap) { @@ -34,11 +32,12 @@ const PlanItem: React.FC = ({ date, day }) => { if (date && tripId) { callBackPub(() => pubGetPathAndItems({ visitDate: date }, tripId)); } + setIsEdit(false); } }, [tap]); const handleEdit = () => { - SetIsEdit((prev) => !prev); + setIsEdit((prev) => !prev); }; const handleTranspo = ( diff --git a/src/components/Plan/PlanMoveItem.tsx b/src/components/Plan/PlanMoveItem.tsx index b06df513..0daa5b5f 100644 --- a/src/components/Plan/PlanMoveItem.tsx +++ b/src/components/Plan/PlanMoveItem.tsx @@ -7,6 +7,8 @@ import { useContext } from 'react'; import { socketContext } from '@hooks/useSocket'; import { useState, useEffect } from 'react'; import ToastPopUp from '@components/common/toastpopup/ToastPopUp'; +import { useRecoilState } from 'recoil'; +import { isEditState } from '@recoil/socket'; interface PlanMoveItemProps { isCheck: number | null; @@ -19,6 +21,7 @@ const PlanMoveItem: React.FC = ({ tripId, visitDate, }) => { + const [, setIsEdit] = useRecoilState(isEditState); const { callBackPub } = useContext(socketContext); const day = useRecoilValue(dayState); const date = useRecoilValue(dateState); @@ -50,6 +53,7 @@ const PlanMoveItem: React.FC = ({ noun: '날짜 이동', verb: '완료', })); + setIsEdit(false); }; useEffect(() => { @@ -81,31 +85,29 @@ const PlanMoveItem: React.FC = ({ - - + +
-
-
-
-

- 날짜 이동 -

-
-
-
- {day.map((day, index) => ( - - - - ))} -
+
+
+

+ 날짜 이동 +

+
+
+
+ {day.map((day, index) => ( + + + + ))}
diff --git a/src/components/Plan/PlanSectionTop.tsx b/src/components/Plan/PlanSectionTop.tsx index d8b60ad9..480ec571 100644 --- a/src/components/Plan/PlanSectionTop.tsx +++ b/src/components/Plan/PlanSectionTop.tsx @@ -6,55 +6,91 @@ import Tab from '@components/common/tab/Tab'; import PlanItem from './PlanItem'; import { socketContext } from '@hooks/useSocket'; import { useContext } from 'react'; -import { pubEnterMember } from '@api/socket'; +import { + pubEnterMember, + pubConnectMember, + pubDisconnectMember, +} from '@api/socket'; import { useEffect } from 'react'; -import { useRecoilValue, useRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { dayState, dateState } from '@recoil/plan'; -import { tripIdState, memberIdState } from '@recoil/socket'; import { calculateDayAndDate } from '@utils/utils'; +import { useGetTrips } from '@hooks/useGetTrips'; +import { visitDateState } from '@recoil/socket'; +import { useState } from 'react'; +import { getItem } from '@utils/localStorageFun'; import PlanSchedule from './PlanSchedule'; const PlanSectionTop = () => { const navigate = useNavigate(); - const tripId = useRecoilValue(tripIdState); - - const pubMember = useRecoilValue(memberIdState); const [, setDay] = useRecoilState(dayState); const [, setDate] = useRecoilState(dateState); - - if (!pubMember || !tripId) { - return
에러
; - } - - const { callBackPub, tripInfo } = useContext(socketContext); - - useEffect(() => { - callBackPub(() => pubEnterMember(tripId)); - }, []); + const { + callBackPub, + tripId, + tripInfo, + tripItem, + tripPath, + tripMember, + tripBudget, + } = useContext(socketContext); + const [, setVisitDate] = useRecoilState(visitDateState); + const { startDate, endDate } = useGetTrips(); + const [isEnter, setIsEnter] = useState(false); let DayArr: string[] = []; let DateArr: string[] = []; - const startDate = tripInfo?.data?.startDate; - const endDate = tripInfo?.data?.endDate; - if (startDate && endDate) { ({ DayArr, DateArr } = calculateDayAndDate(startDate, endDate)); } useEffect(() => { + if (startDate) { + setVisitDate({ visitDate: startDate }); + } setDay(DayArr); setDate(DateArr); }, [startDate, endDate]); + useEffect(() => { + callBackPub(() => pubEnterMember(tripId)); + }, []); + + useEffect(() => { + if (tripInfo && tripItem && tripPath && tripMember && tripBudget) { + setIsEnter(true); + } + }, [tripInfo, tripItem, tripPath, tripMember, tripBudget]); + + useEffect(() => { + if (isEnter) { + const accessToken = getItem('accessToken'); + if (accessToken) { + callBackPub(() => { + pubConnectMember({ token: accessToken || '' }, tripId); + }); + + return () => { + callBackPub(() => + pubDisconnectMember({ token: accessToken || '' }, tripId), + ); + }; + } + } + }, [isEnter]); + return (
{ navigate(-1); }} + showShare={true} + shareHandler={() => { + navigate(`/trip/${tripId}/share`); + }} /> diff --git a/src/components/Plan/TripBudget.tsx b/src/components/Plan/TripBudget.tsx index ee1e5388..0cd9d2a6 100644 --- a/src/components/Plan/TripBudget.tsx +++ b/src/components/Plan/TripBudget.tsx @@ -4,14 +4,11 @@ import { CloseIcon, SettingIcons } from '@components/common/icons/Icons'; import { useGetTripsAuthority } from '@hooks/useGetTripsAuthority'; import { socketContext } from '@hooks/useSocket'; import * as Progress from '@radix-ui/react-progress'; -import { tripIdState } from '@recoil/socket'; import { useContext, useState } from 'react'; -import { useRecoilValue } from 'recoil'; const TripBudget = () => { const { tripAuthority } = useGetTripsAuthority(); - const { tripBudget } = useContext(socketContext); - const tripId = useRecoilValue(tripIdState); + const { tripBudget, tripId } = useContext(socketContext); const budget = tripBudget?.data; @@ -43,11 +40,11 @@ const TripBudget = () => { return ( <> -
+
사용 경비
- {budget?.calculatedPrice.toLocaleString() ?? '-'} + {budget?.calculatedPrice.toLocaleString() ?? 0}
@@ -70,44 +67,47 @@ const TripBudget = () => { /> -
-
- 목표 경비 +
+
+ + 목표 경비 + {tripAuthority == 'WRITE' && ( handleSetTargetBudget(inputBudget)} + onCancel={() => setInputBudget('')} closeOnConfirm={true} children={ } content={ @@ -115,13 +115,13 @@ const TripBudget = () => {
setInputBudget(e.target.value)} />
setInputBudget('')}> {showCloseIcon && ( @@ -129,13 +129,15 @@ const TripBudget = () => {
- + + 원 +
} /> )}
-
+
{budget?.budget.toLocaleString() ?? '- '}
diff --git a/src/components/Share/CodeInput.tsx b/src/components/Share/CodeInput.tsx new file mode 100644 index 00000000..3fca6375 --- /dev/null +++ b/src/components/Share/CodeInput.tsx @@ -0,0 +1,39 @@ +interface Props { + inputCode: string; + setInputCode: React.Dispatch>; + showError: boolean; +} + +const CodeInput = ({ inputCode, setInputCode, showError }: Props) => { + const onCodeChange = async (e: React.ChangeEvent) => { + const changeValue = e.target.value; + if (changeValue.length <= 5) { + setInputCode(e.target.value); + } + }; + return ( +
+
+ +
+ {showError && ( +
+ 편집 참여 코드를 다시 한번 확인해주세요. +
+ )} +
+ ); +}; + +export default CodeInput; diff --git a/src/components/Share/CopyBox.tsx b/src/components/Share/CopyBox.tsx new file mode 100644 index 00000000..7dc6a289 --- /dev/null +++ b/src/components/Share/CopyBox.tsx @@ -0,0 +1,38 @@ +import CopyToast from './CopyToast'; + +interface Props { + title: string; + subTitle: string; + copyValue: string; +} + +const CopyBox = ({ title, subTitle, copyValue }: Props) => { + const onCopyClick = () => { + navigator.clipboard.writeText(copyValue); + }; + + return ( +
+
+ {`${title} 복사`} + {subTitle} +
+
+ + +
+ 복사 +
+
+
+
+ ); +}; + +export default CopyBox; diff --git a/src/components/Share/CopyToast.tsx b/src/components/Share/CopyToast.tsx new file mode 100644 index 00000000..4f7492a3 --- /dev/null +++ b/src/components/Share/CopyToast.tsx @@ -0,0 +1,36 @@ +import * as Toast from '@radix-ui/react-toast'; +import { ReactNode, useState } from 'react'; +import { ReactComponent as CircleCheckIcon } from '@assets/images/CircleCheck.svg'; + +interface Props { + title: string; + children: ReactNode; +} + +const CopyToast = ({ title, children }: Props) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + {`${title}가 복사되었습니다.`} + + + + + ); +}; + +export default CopyToast; diff --git a/src/components/Share/IsEditableModal.tsx b/src/components/Share/IsEditableModal.tsx new file mode 100644 index 00000000..1465ff79 --- /dev/null +++ b/src/components/Share/IsEditableModal.tsx @@ -0,0 +1,68 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { EditStarIcon } from '@components/common/icons/Icons'; +import Alert from '@components/common/alert/Alert'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { getItem } from '@utils/localStorageFun'; + +interface Props { + isEditable: boolean; + setIsEditable: React.Dispatch>; +} + +const IsEditableModal = ({ isEditable, setIsEditable }: Props) => { + const navigate = useNavigate(); + const { pathname } = useLocation(); + + const isLogin = getItem('accessToken'); + + const handleConfirm = () => { + navigate('/login', { state: { prevPath: `${pathname}/code` } }); + }; + + return ( + + + + + + 편집 참여 코드를 입력하시면 +
+ 여행 계획을 함께 편집할 수 있어요! +
+ + + + {isLogin ? ( + + ) : ( + + 편집 참여 코드 입력을 위해 로그인이 필요해요. +
+ 로그인하시겠어요? + + } + onConfirm={handleConfirm}> + +
+ )} + + + +
+
+
+ ); +}; + +export default IsEditableModal; diff --git a/src/components/Trip/EditCodeModal.tsx b/src/components/Trip/EditCodeModal.tsx new file mode 100644 index 00000000..7d3ca648 --- /dev/null +++ b/src/components/Trip/EditCodeModal.tsx @@ -0,0 +1,121 @@ +import { deleteTrips, postTripsjoin } from '@api/trips'; +import CodeInput from '@components/Share/CodeInput'; +import Alert from '@components/common/alert/Alert'; +import { useGetTripsAuthority } from '@hooks/useGetTripsAuthority'; +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import * as Dialog from '@radix-ui/react-dialog'; +import { DeleteIcon, PenIcon } from '@components/common/icons/Icons'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import ToastPopUp from '@components/common/toastpopup/ToastPopUp'; + +const EditCodeModal = () => { + const navigate = useNavigate(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [inputCode, setInputCode] = useState(''); + const [showError, setShowError] = useState(false); + const [isEditModal, setIsEditModal] = useState(false); + const [isToastVisible, setIsToastVisible] = useState(false); + + const { id: tripId } = useParams(); + const { tripAuthority } = useGetTripsAuthority(); + + const queryClient = useQueryClient(); + + const { mutate: deleteMutate } = useMutation({ + mutationFn: (tripId: string) => deleteTrips(tripId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['myTrips'] }); + }, + onError: () => console.log('error'), + }); + + const handleDelete = () => { + if (tripId) { + deleteMutate(tripId); + } + setIsEditModal(false); + setIsToastVisible(true); + setTimeout(() => navigate('/mytrip'), 2000); + }; + + const handleConfirm = async () => { + if (tripId) { + try { + const { data } = await postTripsjoin(tripId, inputCode); + if (data.status === 200) { + setIsModalOpen(false); + } + } catch (err) { + setShowError(true); + setInputCode(''); + console.error('참여 코드 요청 중 에러 발생', err); + } + } + }; + + return ( + <> + {isToastVisible && } + {tripAuthority === 'WRITE' ? ( + + + + + {isEditModal && ( + + + + + 나의 여정 + + + <> + + + + + + + + )} + + ) : ( + + } + isOpen={isModalOpen} + setIsOpen={setIsModalOpen} + onConfirm={handleConfirm}> + + + )} + + ); +}; + +export default EditCodeModal; diff --git a/src/components/Trip/LikedToursList.tsx b/src/components/Trip/LikedToursList.tsx index eab8eeea..5587077e 100644 --- a/src/components/Trip/LikedToursList.tsx +++ b/src/components/Trip/LikedToursList.tsx @@ -15,15 +15,16 @@ export const LikedToursList = () => { >(null); const navigate = useNavigate(); - const params = useParams(); + const { id: tripId } = useParams(); - const [selectedTripId, _] = useState(Number(params.id)); + if (!tripId) { + return; + } const { fetchNextPage, hasNextPage, data, isLoading, error } = useInfiniteQuery({ queryKey: ['ourTrips'], - queryFn: ({ pageParam = 0 }) => - getTripsLike(selectedTripId, pageParam, 10), + queryFn: ({ pageParam = 0 }) => getTripsLike(tripId, pageParam, 10), initialPageParam: 0, getNextPageParam: (lastPage) => { if ( @@ -62,7 +63,7 @@ export const LikedToursList = () => { fetchNextPage={fetchNextPage} hasNextPage={hasNextPage} isLoading={isLoading} - selectedTripId={selectedTripId} + selectedTripId={tripId} selectedContentTypeId={selectedContentTypeId} />
diff --git a/src/components/Trip/LikedToursLists/LikedToursListBox.tsx b/src/components/Trip/LikedToursLists/LikedToursListBox.tsx index 56c2db3d..39ef3aae 100644 --- a/src/components/Trip/LikedToursLists/LikedToursListBox.tsx +++ b/src/components/Trip/LikedToursLists/LikedToursListBox.tsx @@ -12,7 +12,7 @@ interface LikedToursListProps { hasNextPage: boolean; isLoading: boolean; selectedContentTypeId: number | null; - selectedTripId: number; + selectedTripId: string; } const LikedToursListBox: React.FC = ({ diff --git a/src/components/Trip/LikedToursLists/LikedToursListItem.tsx b/src/components/Trip/LikedToursLists/LikedToursListItem.tsx index 8fb54656..7260e8fa 100644 --- a/src/components/Trip/LikedToursLists/LikedToursListItem.tsx +++ b/src/components/Trip/LikedToursLists/LikedToursListItem.tsx @@ -12,7 +12,7 @@ import { useState } from 'react'; interface LikedToursListItemProps { ourTripList: ourTripType; - selectedTripId: number; + selectedTripId: string; } const LikedToursListItem: React.FC = ({ diff --git a/src/components/Trip/TripInfo.tsx b/src/components/Trip/TripInfo.tsx index 8dd648a5..973a38b1 100644 --- a/src/components/Trip/TripInfo.tsx +++ b/src/components/Trip/TripInfo.tsx @@ -5,20 +5,25 @@ import TripSurveyMember from '@components/common/modal/children/TripSurveyMember import { Modal } from '@components/common/modal'; import { useQuery } from '@tanstack/react-query'; import { getTripsMembers } from '@api/trips'; -import { tripIdState } from '@recoil/socket'; // import { ReactComponent as NullUser } from '@assets/images/NullUser.svg'; import { DownIcon } from '@components/common/icons/Icons'; import { useState } from 'react'; import { UserIcon } from '@components/common/icons/Icons'; +import { useGetTripsAuthority } from '@hooks/useGetTripsAuthority'; const ShareList = () => { - const tripId = Number(useRecoilValue(tripIdState)); + const { tripId } = useGetTripsAuthority(); + const { data: tripsMembers } = useQuery({ queryKey: ['tripsMembers', tripId], - queryFn: () => getTripsMembers(tripId), + queryFn: () => + tripId != null + ? getTripsMembers(tripId) + : Promise.reject('tripId is null'), + enabled: !!tripId, }); const members = tripsMembers?.data?.data?.tripMemberSimpleInfos; - + console.log(tripsMembers); return ( <>
@@ -54,15 +59,20 @@ const ShareList = () => { }; const TripInfo = () => { + const { tripId } = useGetTripsAuthority(); const modalChildren = useRecoilValue(modalChildrenState); const [isModalOpen, setIsModalOpen] = useRecoilState(isModalOpenState); - const tripId = Number(useRecoilValue(tripIdState)); const [isAccordion, setIsAccordion] = useState(false); const { data: tripsMembers } = useQuery({ queryKey: ['tripsMembers', tripId], - queryFn: () => getTripsMembers(tripId), + queryFn: () => + tripId != null + ? getTripsMembers(tripId) + : Promise.reject('tripId is null'), + enabled: !!tripId, }); + const members = tripsMembers?.data?.data?.tripMemberSimpleInfos; const closeModal = () => { @@ -73,6 +83,10 @@ const TripInfo = () => { setIsAccordion((prev) => !prev); }; + if (!tripId) { + return
error
; + } + return ( <>
diff --git a/src/components/Trip/TripPreference.tsx b/src/components/Trip/TripPreference.tsx index ca11afda..432c72f2 100644 --- a/src/components/Trip/TripPreference.tsx +++ b/src/components/Trip/TripPreference.tsx @@ -8,9 +8,10 @@ import { } from '@utils/calculatePercentage'; import { modalChildrenState, isModalOpenState } from '@recoil/modal'; import { getTripsSurveyMembers } from '@api/trips'; -import { tripIdState } from '@recoil/socket'; -import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil'; +import { useSetRecoilState, useRecoilState } from 'recoil'; import { participantsState } from '@recoil/trip'; +import { useGetTripsAuthority } from '@hooks/useGetTripsAuthority'; + interface RatioBarParams { value: number; total: number; @@ -96,6 +97,7 @@ const Percentage = ({ value, total, color }: PercentageParams) => ( ); const TripPreference: React.FC = () => { + const { tripId } = useGetTripsAuthority(); const [A, setA] = useState<[number, number]>([0, 0]); const [B, setB] = useState<[number, number]>([0, 0]); const [C, setC] = useState<[number, number]>([0, 0]); @@ -103,12 +105,15 @@ const TripPreference: React.FC = () => { const [E, setE] = useState<[number, number]>([0, 0]); const setModalChildren = useSetRecoilState(modalChildrenState); const setIsModalOpen = useSetRecoilState(isModalOpenState); - const tripId = Number(useRecoilValue(tripIdState)); const [participants, setParticipants] = useRecoilState(participantsState); const { data: tripsSurveyMembers } = useQuery({ queryKey: ['tripsSurveyMembers', tripId], - queryFn: () => getTripsSurveyMembers(tripId), + queryFn: () => + tripId != null + ? getTripsSurveyMembers(tripId) + : Promise.reject('tripId is null'), + enabled: !!tripId, }); useEffect(() => { @@ -118,7 +123,11 @@ const TripPreference: React.FC = () => { const { data: tripPreference, isLoading } = useQuery({ queryKey: ['tripPreference', tripId], - queryFn: () => getTripsSurvey(tripId), + queryFn: () => + tripId != null + ? getTripsSurvey(tripId) + : Promise.reject('tripId is null'), + enabled: !!tripId, }); useEffect(() => { diff --git a/src/components/Trip/TripRealtimeEditor.tsx b/src/components/Trip/TripRealtimeEditor.tsx index 895b51e4..3e47bee2 100644 --- a/src/components/Trip/TripRealtimeEditor.tsx +++ b/src/components/Trip/TripRealtimeEditor.tsx @@ -1,46 +1,14 @@ -import { useRecoilValue } from 'recoil'; -import { tripIdState } from '@recoil/socket'; -import { useEffect, useState } from 'react'; import { socketContext } from '@hooks/useSocket'; import { useContext } from 'react'; -import { pubConnectMember, pubDisconnectMember } from '@api/socket'; import { UserIcon } from '@components/common/icons/Icons'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Navigation } from 'swiper/modules'; -import { getItem } from '@utils/localStorageFun'; const TripRealtimeEditor = () => { - const tripId = useRecoilValue(tripIdState); - const { callBackPub, tripMember } = useContext(socketContext); - const [token, setToken] = useState(''); - const [pubMember, setPubMember] = useState({ token: '' }); - - useEffect(() => { - const accessToken = getItem('accessToken'); - if (accessToken) { - setToken(accessToken); - } - }, []); - - useEffect(() => { - setPubMember({ token: token || '' }); - }, [token]); - - useEffect(() => { - if (pubMember && tripId) { - callBackPub(() => { - pubConnectMember(pubMember, tripId); - }); - return () => { - callBackPub(() => pubDisconnectMember(pubMember, tripId)); - }; - } - }, [pubMember]); + const { tripMember } = useContext(socketContext); const tripMemberData = tripMember?.data; - useEffect(() => { - console.log('tripMemberData', tripMemberData); - }, [tripMemberData]); + return (
{ + const { tripName, startDate, endDate, numberOfPeople } = useGetTrips(); + return ( <>
-
-
강릉 여행 일정
+
+
{tripName}
- 5 + {numberOfPeople}
+ +
- 23.12.23 - 23.12.25 + + {startDate?.substring(2).replace(/-/g, '.') || ''} -{' '} + {endDate?.substring(5).replace(/-/g, '.') || ''} + ); }; diff --git a/src/components/Trip/TripSectionTop.tsx b/src/components/Trip/TripSectionTop.tsx index 59ef58eb..d1388108 100644 --- a/src/components/Trip/TripSectionTop.tsx +++ b/src/components/Trip/TripSectionTop.tsx @@ -6,21 +6,34 @@ import { useNavigate } from 'react-router-dom'; import PlanTripButton from './PlanTripButton'; import { LikedToursList } from './LikedToursList'; import { useGetTripsAuthority } from '@hooks/useGetTripsAuthority'; +import { useEffect, useState } from 'react'; +import IsEditableModal from '@components/Share/IsEditableModal'; const TripSectionTop = () => { const navigate = useNavigate(); const { tripAuthority } = useGetTripsAuthority(); + const [isEditable, setIsEditable] = useState(false); - console.log(tripAuthority); + useEffect(() => { + if (tripAuthority !== null) { + if (tripAuthority !== 'WRITE') { + setIsEditable(true); + } + } + }, [tripAuthority]); return (
+ { navigate(-1); }} + showShare={true} + shareHandler={() => { + navigate('share'); + }} /> diff --git a/src/components/common/BackBox/BackBox.tsx b/src/components/common/BackBox/BackBox.tsx index 4af0db0f..0d29c96f 100644 --- a/src/components/common/BackBox/BackBox.tsx +++ b/src/components/common/BackBox/BackBox.tsx @@ -10,6 +10,7 @@ interface Props { showSave?: boolean; saveHandler?: VoidFunction; showShare?: boolean; + shareHandler?: VoidFunction; } const BackBox = ({ @@ -21,6 +22,7 @@ const BackBox = ({ showSave, saveHandler, showShare, + shareHandler, }: Props) => { const onBackClick = () => { backHandler && backHandler(); @@ -55,7 +57,9 @@ const BackBox = ({ )} {showShare && ( - )} diff --git a/src/components/common/alert/Alert.tsx b/src/components/common/alert/Alert.tsx index df7f1dd1..dbb26a3f 100644 --- a/src/components/common/alert/Alert.tsx +++ b/src/components/common/alert/Alert.tsx @@ -3,13 +3,15 @@ import * as Dialog from '@radix-ui/react-dialog'; interface AlertProps { title: string; - message: ReactNode; + message?: ReactNode; onConfirm: (() => void) | ((e: React.MouseEvent) => void); onCancel?: (() => void) | ((e: React.MouseEvent) => void); children: ReactNode; content?: ReactNode; closeOnConfirm?: boolean; isCheck?: number | null; + isOpen?: boolean; + setIsOpen?: React.Dispatch>; } const Alert: FC = ({ @@ -21,8 +23,10 @@ const Alert: FC = ({ content, closeOnConfirm = false, isCheck = true, + isOpen, + setIsOpen, }) => ( - + {children} @@ -31,10 +35,10 @@ const Alert: FC = ({ + } fixed left-[50%] top-[50%] z-[130] flex min-h-[192px] w-[309px] translate-x-[-50%] translate-y-[-50%] flex-col items-center justify-center rounded-2xl bg-white px-[14px] shadow-lg focus:outline-none`}>

= ({ diff --git a/src/components/common/button/ListSelectBtn.tsx b/src/components/common/button/ListSelectBtn.tsx index 4328e6d2..e4f439df 100644 --- a/src/components/common/button/ListSelectBtn.tsx +++ b/src/components/common/button/ListSelectBtn.tsx @@ -9,8 +9,9 @@ interface ListSelectBtnProps { export const ListSelectBtn = ({ children, onClick }: ListSelectBtnProps) => { const [isActive, setIsActive] = useState(false); - const handleClick = () => { + const handleClick = (e: React.MouseEvent) => { setIsActive(!isActive); + e.stopPropagation(); if (onClick) { onClick(); } @@ -36,15 +37,16 @@ interface ListCheckBtnProps { export const ListCheckBtn = ({ onClick }: ListCheckBtnProps) => { const [isActive, setIsActive] = useState(false); - const handleClick = () => { + const handleClick = (e: React.MouseEvent) => { setIsActive(!isActive); + e.stopPropagation(); if (onClick) { onClick(); } }; return ( -

+
+ )} +
{numOfMembers}
+ +
+ + + { + setShowSelectDate(true); + }} + isClickable> +
{formattedTripDate}
+
+ +
+ 완료 +
+
+ ); +}; + +export default TripEdit; diff --git a/src/recoil/socket.ts b/src/recoil/socket.ts index ab7bb76b..f8a995c9 100644 --- a/src/recoil/socket.ts +++ b/src/recoil/socket.ts @@ -1,16 +1,11 @@ import { atom } from 'recoil'; -export const tripIdState = atom({ - key: 'tripIdState', - default: '1', -}); - export const visitDateState = atom<{ visitDate: string } | null>({ key: 'visitDateState', - default: { visitDate: '2024-01-03' }, + default: { visitDate: '' }, }); -export const memberIdState = atom<{ token: number | null }>({ - key: 'memberIdState', - default: { token: null }, +export const isEditState = atom({ + key: 'isEditState', + default: false, }); diff --git a/src/router/routerLayout.tsx b/src/router/routerLayout.tsx index 84f02529..75d0006f 100644 --- a/src/router/routerLayout.tsx +++ b/src/router/routerLayout.tsx @@ -24,9 +24,9 @@ const MainLayout = () => { ); return ( -
+
-
+
{showNav &&