diff --git a/package.json b/package.json index 753474f8..2a63f1a5 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,13 @@ "@svgr/rollup": "^8.1.0", "@tanstack/react-query": "^5.14.6", "@tanstack/react-query-devtools": "^5.14.6", + "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.6.2", "date-fns": "^3.1.0", "msw": "0.36.3", "path": "^0.12.7", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-hook-form": "^7.49.2", "react-infinite-scroller": "^1.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d42035a..753ad932 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ dependencies: '@tanstack/react-query-devtools': specifier: ^5.14.6 version: 5.15.0(@tanstack/react-query@5.15.0)(react@18.2.0) + '@types/react-beautiful-dnd': + specifier: ^13.1.8 + version: 13.1.8 axios: specifier: ^1.6.2 version: 1.6.3 @@ -62,6 +65,9 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -2959,6 +2965,13 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: false + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.45 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/inquirer@8.2.10: resolution: {integrity: sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==} dependencies: @@ -2983,6 +2996,12 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + /@types/react-beautiful-dnd@13.1.8: + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + dependencies: + '@types/react': 18.2.45 + dev: false + /@types/react-date-range@1.4.9: resolution: {integrity: sha512-5oVEDW0ElYmY1+YVSzdMUR8stxSI5QrRJCgCFUvuEAV5197t412vimD9aVTW6g4JTaxCnMmB1BdEOT/odpaBxQ==} dependencies: @@ -3007,6 +3026,15 @@ packages: '@types/react': 18.2.45 dev: true + /@types/react-redux@7.1.33: + resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.45 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + /@types/react-scroll@1.8.10: resolution: {integrity: sha512-RD4Z7grbdNGOKwKnUBKar6zNxqaW3n8m9QSrfvljW+gmkj1GArb8AFBomVr6xMOgHPD3v1uV3BrIf01py57daQ==} dependencies: @@ -3606,6 +3634,12 @@ packages: which: 2.0.2 dev: true + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.1 + dev: false + /css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} @@ -4263,6 +4297,12 @@ packages: resolution: {integrity: sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ==} dev: false + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + /http-parser-js@0.5.8: resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} dev: false @@ -4555,6 +4595,10 @@ packages: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: false + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -5024,6 +5068,29 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.6 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -5056,6 +5123,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + /react-kakao-maps-sdk@1.1.24(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-leLbFwBj6zbTdDg6A9U7EwYT2oq0+2F+NHZSVTyCmmvyc4yt2zpRvUmcAt8I6h2SDUdgHbpvKAV1sZoRIxD4JQ==} peerDependencies: @@ -5087,6 +5158,28 @@ packages: warning: 4.0.3 dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -5228,6 +5321,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.23.6 + dev: false + /regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -5658,6 +5757,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -5806,6 +5909,14 @@ packages: tslib: 2.6.2 dev: false + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /use-sidecar@1.1.2(@types/react@18.2.45)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} diff --git a/src/@types/service.ts b/src/@types/service.ts index 8706a8da..cf8f405b 100644 --- a/src/@types/service.ts +++ b/src/@types/service.ts @@ -120,3 +120,11 @@ export type TripItem = { visitDate: string; price: number; }; + +export interface pubUpdateTripItemReq { + visitDate: string; + tripItemOrder: { + tripItemId: number; + seqNum: number; + }[]; +} diff --git a/src/@types/socket.types.ts b/src/@types/socket.types.ts index be9c82fa..9434e981 100644 --- a/src/@types/socket.types.ts +++ b/src/@types/socket.types.ts @@ -121,13 +121,13 @@ interface pubUpdateTransportation { } interface pubVisitDate { - tripId: number; + tripId: string; oldVisitDate: string; newVisitDate: string; } interface pubDeleteItem { - tripId: number; + tripId: string; visitDate: string; } diff --git a/src/api/socket.ts b/src/api/socket.ts index 94033e68..2be1d259 100644 --- a/src/api/socket.ts +++ b/src/api/socket.ts @@ -2,6 +2,8 @@ import * as StompJs from '@stomp/stompjs'; export const socketClient = new StompJs.Client({ brokerURL: import.meta.env.VITE_SOCKET_URL, + heartbeatIncoming: 1000, + heartbeatOutgoing: 1000, }); // 소켓 구독 @@ -99,6 +101,9 @@ export const pubUpdateTripItem = ( destination: `/pub/trips/${tripId}/updateTripItemOrder`, body: JSON.stringify(pubUpdateTripItem), }); + + console.log(pubUpdateTripItem); + console.log('펍실행'); }; // 여행 날짜별 교통 수단 변경 이벤트 발생시 (01/16 업데이트) @@ -132,6 +137,7 @@ export const pubDeleteItem = ( destination: `/pub/tripItems/${tripItemId}/deleteItem`, body: JSON.stringify(pubDeleteItem), }); + console.log(pubDeleteItem); }; // 멤버 여정 페이지로 입장 이벤트 발생시 @@ -163,7 +169,6 @@ export const pubGetPathAndItems = ( pubGetPathAndItems: pubGetPathAndItems, tripId: string, ) => { - console.log('펍내부',pubGetPathAndItems); socketClient.publish({ destination: `/pub/trips/${tripId}/getPathAndItems`, body: JSON.stringify(pubGetPathAndItems), diff --git a/src/api/trips.ts b/src/api/trips.ts index 3441cb6b..be019a7f 100644 --- a/src/api/trips.ts +++ b/src/api/trips.ts @@ -47,7 +47,13 @@ export const getTripsLike = async ( // 우리의 관심 목록 등록 export const postTripsLike = async (tripId: number, tourItemIds: number[]) => { - const res = await client.post(`trips/${tripId}/tripLikedTours`, tourItemIds); + const requestBody = { + tourItemIds: tourItemIds, + }; + const res = await authClient.post( + `trips/${tripId}/tripLikedTours`, + requestBody, + ); return res; }; @@ -68,3 +74,13 @@ export const getTripsSurvey = async (tripId: number) => { const res = await client.get(`trips/${tripId}/survey`); return res; }; +// 우리의 여행취향 참여/미참여 회원 조회 +export const getTripsSurveyMembers = async (tripId: number) => { + const res = await client.get(`trips/${tripId}/survey/members`); + return res; +}; +// 여정을 공유하고 있는 회원 조회 +export const getTripsMembers = async (tripId: number) => { + const res = await client.get(`trips/${tripId}/members`); + return res; +}; diff --git a/src/components/DetailSectionTop/DetailAddSchedule.tsx b/src/components/DetailSectionTop/DetailAddSchedule.tsx index d805968c..d6fac6dd 100644 --- a/src/components/DetailSectionTop/DetailAddSchedule.tsx +++ b/src/components/DetailSectionTop/DetailAddSchedule.tsx @@ -38,21 +38,21 @@ const DetailAddSchedule = () => { className="h-[52px] w-[52px] flex-shrink-0 flex-grow-0 rounded-lg object-cover" />
-

+

강릉 속초 여행 -

-

+

+
2023.12.20 - 12.22 (3박 4일) -

+
-

+

Day 1 -

+
diff --git a/src/components/Plan/PlanEditItemBox.tsx b/src/components/Plan/PlanEditItemBox.tsx new file mode 100644 index 00000000..ae5f4873 --- /dev/null +++ b/src/components/Plan/PlanEditItemBox.tsx @@ -0,0 +1,190 @@ +import { PenIcon, DragAndDropIcon } from '@components/common/icons/Icons'; +import { TripItem } from '@/@types/service'; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from 'react-beautiful-dnd'; +import { useState, useEffect } from 'react'; +import { pubUpdateTripItem, pubDeleteItem } from '@api/socket'; +import { useContext } from 'react'; +import { socketContext } from '@hooks/useSocket'; +import { pubUpdateTripItemReq } from '@/@types/service'; +import Alert from '@components/common/alert/Alert'; +import ToastPopUp from '@components/common/toastpopup/ToastPopUp'; +import PlanMoveItem from './PlanMoveItem'; + +type PlanItemBoxProps = { + item: TripItem[]; + day: string; + visitDate: string; + tripId: string; +}; + +const PlanEditItemBox = ({ + item, + day, + visitDate, + tripId, +}: PlanItemBoxProps) => { + if (!item) { + return
Missing data
; + } + + const { callBackPub } = useContext(socketContext); + + const [items, setItems] = useState(item); + const [newData, setNewData] = useState(null); + const [selectedItemId, setSelectedItemId] = useState(null); + const [toastPopUp, setToastPopUp] = useState({ + isPopUp: false, + noun: '', + verb: '', + }); + + const onDragEnd = (result: DropResult) => { + if (!result.destination) return; + const reorderedItems = Array.from(items); + const [relocatedItem] = reorderedItems.splice(result.source.index, 1); + reorderedItems.splice(result.destination.index, 0, relocatedItem); + setItems(reorderedItems); + + const tripItemOrder = reorderedItems.map((item, index) => ({ + tripItemId: item.tripItemId, + seqNum: index + 1, + })); + + setNewData({ + visitDate: visitDate, + tripItemOrder, + }); + + console.log(newData); + }; + + useEffect(() => { + if (newData && tripId) { + callBackPub(() => pubUpdateTripItem(newData, tripId)); + } + }, [newData]); + + const handleConfirm = () => { + if (tripId && visitDate && selectedItemId) { + callBackPub(() => + pubDeleteItem({ tripId: tripId, visitDate: visitDate }, selectedItemId), + ); + } + setToastPopUp(() => ({ + isPopUp: true, + noun: '여행지', + verb: '삭제', + })); + }; + + const handleRadioChange = (id: number | null) => { + setSelectedItemId(id); + }; + + useEffect(() => { + if (toastPopUp.isPopUp) { + const timer = setTimeout(() => { + setToastPopUp(() => ({ + isPopUp: false, + noun: '', + verb: '', + })); + }, 2000); + return () => clearTimeout(timer); + } + }, [toastPopUp]); + + return ( + <> + {toastPopUp.isPopUp && ( + + )} + + + {(provided) => ( +
+
{day}
+ {items.map((item, index) => ( + + {(provided) => ( +
+
+ handleRadioChange(item.tripItemId)} + checked={selectedItemId === item.tripItemId}> +
+
+
+ img +
+
+ {item.name} + +
+
+ {item.category} +
+
+ {item.price} 원 +
+
+
+
+
+ +
+
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+
+ 선택한 장소를 삭제하시겠습니까?} + onConfirm={handleConfirm} + closeOnConfirm={true} + isCheck={selectedItemId}> + + + +
+
+ + ); +}; + +export default PlanEditItemBox; diff --git a/src/components/Plan/PlanItem.tsx b/src/components/Plan/PlanItem.tsx index 89a28752..c27cf354 100644 --- a/src/components/Plan/PlanItem.tsx +++ b/src/components/Plan/PlanItem.tsx @@ -3,7 +3,8 @@ import { PlusIcon, CarIcon, BusIcon } from '@components/common/icons/Icons'; import { useNavigate } from 'react-router-dom'; import TripMap from './TripMap'; import PlanItemBox from './PlanItemBox'; -import { useContext, useEffect } from 'react'; +import PlanEditItemBox from './PlanEditItemBox'; +import { useContext, useEffect, useState } from 'react'; import { socketContext } from '@hooks/useSocket'; import { useRecoilState } from 'recoil'; import { visitDateState } from '@recoil/socket'; @@ -11,15 +12,22 @@ import { pubGetPathAndItems, pubUpdateTransportation } from '@api/socket'; import { tripIdState } from '@recoil/socket'; import { useRecoilValue } from 'recoil'; -const PlanItem = (date: any) => { +type PlanItemProps = { + date: string; + day: string; +}; + +const PlanItem: React.FC = ({ date, day }) => { const navigate = useNavigate(); + const [isEdit, SetIsEdit] = useState(false); + const tripId = useRecoilValue(tripIdState); const [visitDate, setVisitDate] = useRecoilState(visitDateState); const { tripItem, tripPath, callBackPub } = useContext(socketContext); useEffect(() => { - setVisitDate({ visitDate: date.date }); - }, [date.date]); + setVisitDate({ visitDate: date }); + }, [date]); useEffect(() => { if (visitDate && tripId) { @@ -27,6 +35,10 @@ const PlanItem = (date: any) => { } }, [visitDate]); + const handleEdit = () => { + SetIsEdit((prev) => !prev); + }; + const handleTranspo = ( transportation: 'CAR' | 'PUBLIC_TRANSPORTATION', visitDate: string, @@ -52,55 +64,77 @@ const PlanItem = (date: any) => { {tripPath && }
-
-
- handleTranspo('CAR', visitDate?.visitDate || '', tripId || '') - } - className="flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-l-md border border-solid border-gray3"> - -
-
- handleTranspo( - 'PUBLIC_TRANSPORTATION', - visitDate?.visitDate || '', - tripId || '', - ) - } - className="pointer-cursor -ml-[1px] flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-r-md border border-solid border-gray3"> - + ) : ( +
+
+ handleTranspo('CAR', visitDate?.visitDate || '', tripId || '') + } + className="flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-l-md border border-solid border-gray3"> + +
+
+ handleTranspo( + 'PUBLIC_TRANSPORTATION', + visitDate?.visitDate || '', + tripId || '', + ) } - /> + className="pointer-cursor -ml-[1px] flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-r-md border border-solid border-gray3"> + +
-
-
- + {isEdit ? ( + + ) : ( + + )}
- navigate('./place')} - className="h-[40px] w-full"> -
- -
장소 추가하기
-
-
+ {isEdit ? ( + '' + ) : ( + navigate('./place')} + className="h-[40px] w-full"> +
+ +
장소 추가하기
+
+
+ )}
); diff --git a/src/components/Plan/PlanItemBox.tsx b/src/components/Plan/PlanItemBox.tsx index 217e96c2..d54a922c 100644 --- a/src/components/Plan/PlanItemBox.tsx +++ b/src/components/Plan/PlanItemBox.tsx @@ -1,4 +1,9 @@ -import { PenIcon, CarIcon, BusIcon } from '@components/common/icons/Icons'; +import { + PenIcon, + CarIcon, + BusIcon, + SequenceIcon, +} from '@components/common/icons/Icons'; import { TripItem, Paths } from '@/@types/service'; import { v4 as uuidv4 } from 'uuid'; @@ -6,9 +11,15 @@ type PlanItemBoxProps = { item: TripItem[]; paths: Paths[]; transportation: string; + day: string; }; -const PlanItemBox = ({ item, paths, transportation }: PlanItemBoxProps) => { +const PlanItemBox = ({ + item, + paths, + transportation, + day, +}: PlanItemBoxProps) => { if (!item || !paths) { return
Missing data
; } @@ -18,53 +29,62 @@ const PlanItemBox = ({ item, paths, transportation }: PlanItemBoxProps) => { return ( <>
+
{day}
{item.map((item, index) => ( - <> -
- img -
-
- {item.name} - -
-
- {item.category} -
-
- {item.price} 원 +
+
+ {index !== 0 ? ( +
+ ) : ( +
+ )} + +
+
+
+ img +
+
+ {item.name} + +
+
+ {item.category} +
+
+ {item.price} 원 +
-
- {index < itemLength - 1 && - paths - .filter((path) => path.fromSeqNum === item.seqNum) - .map((path) => ( -
-
-
- {transportation === 'CAR' ? ( - - ) : transportation === 'PUBLIC_TRANSPORTATION' ? ( - - ) : null} -
-
- {(path.pathInfo.totalDistance / 1000).toFixed(2)}km,{' '} - {path.pathInfo.totalTime}분,{' '} - {path.pathInfo.price.toLocaleString()}원 + {index < itemLength - 1 && + paths + .filter((path) => path.fromSeqNum === item.seqNum) + .map((path) => ( +
+
+
+ {transportation === 'CAR' ? ( + + ) : transportation === 'PUBLIC_TRANSPORTATION' ? ( + + ) : null} +
+
+ {(path.pathInfo.totalDistance / 1000).toFixed(2)}km,{' '} + {path.pathInfo.totalTime}분,{' '} + {path.pathInfo.price.toLocaleString()}원 +
-
- ))} - + ))} +
+
))}
diff --git a/src/components/Plan/PlanMoveItem.tsx b/src/components/Plan/PlanMoveItem.tsx new file mode 100644 index 00000000..9601061a --- /dev/null +++ b/src/components/Plan/PlanMoveItem.tsx @@ -0,0 +1,122 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { PaperIcon } from '@components/common/icons/Icons'; +import { useRecoilValue } from 'recoil'; +import { dayState, dateState } from '@recoil/plan'; +import { pubUpdateVisitDate } from '@api/socket'; +import { useContext } from 'react'; +import { socketContext } from '@hooks/useSocket'; +import { useState, useEffect } from 'react'; +import ToastPopUp from '@components/common/toastpopup/ToastPopUp'; + +interface PlanMoveItemProps { + isCheck: number | null; + tripId: string | null; + visitDate: string | null; +} + +const PlanMoveItem: React.FC = ({ + isCheck, + tripId, + visitDate, +}) => { + const { callBackPub } = useContext(socketContext); + const day = useRecoilValue(dayState); + const date = useRecoilValue(dateState); + + const [toastPopUp, setToastPopUp] = useState({ + isPopUp: false, + noun: '', + verb: '', + }); + + const handleMoveItem = (newVisitDate: string) => { + if (visitDate === newVisitDate) { + return; + } + if (tripId && isCheck && visitDate) { + callBackPub(() => + pubUpdateVisitDate( + { + tripId: tripId, + oldVisitDate: visitDate, + newVisitDate: newVisitDate, + }, + isCheck, + ), + ); + } + setToastPopUp(() => ({ + isPopUp: true, + noun: '날짜 이동', + verb: '완료', + })); + }; + + useEffect(() => { + if (toastPopUp.isPopUp) { + const timer = setTimeout(() => { + setToastPopUp(() => ({ + isPopUp: false, + noun: '', + verb: '', + })); + }, 2000); + return () => clearTimeout(timer); + } + }, [toastPopUp]); + + return ( + <> + {toastPopUp.isPopUp && ( + + )} + + + + + + + + + +
+
+
+
+

+ 날짜 이동 +

+
+
+
+ {day.map((day, index) => ( + + + + ))} +
+
+
+
+
+
+
+
+
+ + ); +}; + +export default PlanMoveItem; diff --git a/src/components/Plan/PlanSectionTop.tsx b/src/components/Plan/PlanSectionTop.tsx index 37224884..c8ace510 100644 --- a/src/components/Plan/PlanSectionTop.tsx +++ b/src/components/Plan/PlanSectionTop.tsx @@ -8,7 +8,8 @@ import { socketContext } from '@hooks/useSocket'; import { useContext } from 'react'; import { pubEnterMember } from '@api/socket'; import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useRecoilState } from 'recoil'; +import { dayState, dateState } from '@recoil/plan'; import { tripIdState, memberIdState } from '@recoil/socket'; import { calculateDayAndDate } from '@utils/utils'; @@ -16,6 +17,8 @@ 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
에러
; @@ -36,7 +39,12 @@ const PlanSectionTop = () => { if (startDate && endDate) { ({ DayArr, DateArr } = calculateDayAndDate(startDate, endDate)); } - + + useEffect(() => { + setDay(DayArr); + setDate(DateArr); + }, [startDate, endDate]); + return (
{ ( - + contents={DateArr.map((date, index) => ( + ))} />
diff --git a/src/components/Trip/PlanTripButton.tsx b/src/components/Trip/PlanTripButton.tsx index b218ef76..bd5ac6fc 100644 --- a/src/components/Trip/PlanTripButton.tsx +++ b/src/components/Trip/PlanTripButton.tsx @@ -2,12 +2,18 @@ import { PlanIcon, RightIcon } from '@components/common/icons/Icons'; const PlanTripButton = () => { return ( - ); }; diff --git a/src/components/Trip/TripInfo.tsx b/src/components/Trip/TripInfo.tsx index 8e7a2a21..5594c89a 100644 --- a/src/components/Trip/TripInfo.tsx +++ b/src/components/Trip/TripInfo.tsx @@ -1,28 +1,128 @@ import { UserIcon } from '@components/common/icons/Icons'; -import { socketContext } from '@hooks/useSocket'; -import { useContext } from 'react'; +import { useRecoilValue, useRecoilState } from 'recoil'; +import { isModalOpenState, modalChildrenState } from '@recoil/modal'; +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'; + +const ShareList = () => { + const tripId = Number(useRecoilValue(tripIdState)); + const { data: tripsMembers } = useQuery({ + queryKey: ['tripsMembers', tripId], + queryFn: () => getTripsMembers(tripId), + }); + const members = tripsMembers?.data?.data?.tripMemberSimpleInfos; + + return ( + <> +
+
+ {members.map((member: any, index: number) => { + return ( +
+ {member.profileImageUrl && + member.profileImageUrl !== 'http://asiduheimage.jpg' ? ( + 유저 프로필 + ) : ( + + )} +
{member.nickname}
+
+ ); + })} +
+ + ); +}; const TripInfo = () => { - const { tripInfo } = useContext(socketContext); - const trip = tripInfo?.data; + 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), + }); + const members = tripsMembers?.data?.data?.tripMemberSimpleInfos; + + const closeModal = () => { + setIsModalOpen(false); + }; + + const handleClickButton = () => { + setIsAccordion((prev) => !prev); + }; return ( -
-
-
-
{trip?.tripName}
-
- - - {trip?.numberOfPeople} - + <> +
+
+
+ {members?.map((member: any, index: number) => ( +
+ {member.profileImageUrl && + member.profileImageUrl !== 'http://asiduheimage.jpg' ? ( + 유저 프로필 + ) : ( + + )} +
+ ))} +
+ +
+

+ {members?.length}명과 공유중 +

+
+ +
+
+
+ + {isAccordion && } +
+
+
+
강릉 여행 일정
+
+ + 5 +
+
+ 23.12.23 - 23.12.25
- - {trip?.startDate} ~ {trip?.endDate} - -
+ + {modalChildren === 'TripSurveyMember' && } + + ); }; diff --git a/src/components/Trip/TripParticipant.tsx b/src/components/Trip/TripParticipant.tsx new file mode 100644 index 00000000..934aa95e --- /dev/null +++ b/src/components/Trip/TripParticipant.tsx @@ -0,0 +1,51 @@ +import { ReactComponent as NullUser } from '@assets/images/NullUser.svg'; +import { useRecoilValue } from 'recoil'; +import { participantsState } from '@recoil/trip'; + +interface ParticipantStatusProps { + status: string; +} + +const ParticipantList: React.FC<{ infos: any[] }> = ({ infos }) => ( +
+ {infos.map((info: any) => ( +
+ {info.thumbnail && info.thumbnail !== 'http://asiduheimage.jpg' ? ( + 유저 프로필 + ) : ( + + )} +
{info.nickname}
+
+ ))} +
+); + +export const ParticipantStatus: React.FC = ({ + status, +}) => { + const participants = useRecoilValue(participantsState); + + return ( +
+
+ {status == '참여' ? ( + <>{participants?.tripSurveyMemberCount}명 참여 + ) : ( + <>{participants?.nonTripSurveySetMemberInfos?.length}명 미참여 + )} +
+ {status == '참여' ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/src/components/Trip/TripPreference.tsx b/src/components/Trip/TripPreference.tsx index 0a59f8c7..02f71976 100644 --- a/src/components/Trip/TripPreference.tsx +++ b/src/components/Trip/TripPreference.tsx @@ -1,14 +1,16 @@ import React, { useState, useEffect } from 'react'; import { getTripsSurvey } from '@api/trips'; import { useQuery } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; -import { MoreIcon } from '@components/common/icons/Icons'; -import { RightIcon } from '@components/common/icons/Icons'; +import { MoreIcon, RightIcon, HeartIcon } from '@components/common/icons/Icons'; import { calculatePercentage, calculatePercentageRemain, } 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 { participantsState } from '@recoil/trip'; interface RatioBarParams { value: number; total: number; @@ -26,7 +28,12 @@ interface PercentageParams { const TripPreferenceButton: React.FC = () => { return ( - + {closeOnConfirm ? ( + + + + ) : ( + + )}
diff --git a/src/components/common/button/ListSelectBtn.tsx b/src/components/common/button/ListSelectBtn.tsx new file mode 100644 index 00000000..4328e6d2 --- /dev/null +++ b/src/components/common/button/ListSelectBtn.tsx @@ -0,0 +1,57 @@ +import { useState, ReactNode } from 'react'; +import { CheckIcon } from '../icons/Icons'; + +interface ListSelectBtnProps { + children: ReactNode; + onClick?: () => void; +} + +export const ListSelectBtn = ({ children, onClick }: ListSelectBtnProps) => { + const [isActive, setIsActive] = useState(false); + + const handleClick = () => { + setIsActive(!isActive); + if (onClick) { + onClick(); + } + }; + + return ( + + ); +}; + +interface ListCheckBtnProps { + onClick?: () => void; +} + +export const ListCheckBtn = ({ onClick }: ListCheckBtnProps) => { + const [isActive, setIsActive] = useState(false); + + const handleClick = () => { + setIsActive(!isActive); + if (onClick) { + onClick(); + } + }; + + return ( +
+ +
+ ); +}; diff --git a/src/components/common/icons/Icons.tsx b/src/components/common/icons/Icons.tsx index 3d70d69e..34987b1f 100644 --- a/src/components/common/icons/Icons.tsx +++ b/src/components/common/icons/Icons.tsx @@ -7,6 +7,7 @@ interface IconProps { isHalf?: boolean; cursor?: string; children?: React.ReactNode; + number?: number; } export const HomeIcon: React.FC = ({ @@ -1297,3 +1298,204 @@ export const CounterIcon: React.FC< ); }; + +export const RedIcon: React.FC = ({ size = 20, className }) => { + return ( + + + + + ); +}; + +export const VioletIcon: React.FC = ({ size = 20, className }) => { + return ( + + + + + ); +}; + +export const CyanIcon: React.FC = ({ size = 20, className }) => { + return ( + + + + + ); +}; + +export const OrangeIcon: React.FC = ({ size = 20, className }) => { + return ( + + + + + ); +}; + +export const GreenIcon: React.FC = ({ size = 20, className }) => { + return ( + + + + + ); +}; + +export const SequenceIcon: React.FC = ({ + size = 20, + className, + number, +}) => { + return ( + + + {number !== undefined && ( + + {number} + + )} + + ); +}; + +export const DragAndDropIcon: React.FC = ({}) => { + return ( + + + + ); +}; + +export const PaperIcon: React.FC = ({}) => { + return ( + + + + + + + ); +}; diff --git a/src/components/common/modal/Modal.tsx b/src/components/common/modal/Modal.tsx index 34060d49..c8ec7ed8 100644 --- a/src/components/common/modal/Modal.tsx +++ b/src/components/common/modal/Modal.tsx @@ -69,5 +69,25 @@ export const getModalStyles = (modalChildren: string) => { zIndex: 1, // 이거 해줘야 kakao-map도 dimmed됨 }, }; + } else if (modalChildren === 'TripSurveyMember') { + return { + content: { + top: 'auto', + left: '50%', + right: 'auto', + bottom: '0', + marginRight: '-50%', + transform: 'translate(-50%, 0)', + maxWidth: '412px', + width: '100%', + height: '280px', + borderTopLeftRadius: '2rem', + borderTopRightRadius: '2rem', + }, + overlay: { + backgroundColor: 'rgba(0, 0, 0, 0.25)', + zIndex: 1, // 이거 해줘야 kakao-map도 dimmed됨 + }, + }; } }; diff --git a/src/components/common/modal/children/TripSurveyMember.tsx b/src/components/common/modal/children/TripSurveyMember.tsx new file mode 100644 index 00000000..ee6551c6 --- /dev/null +++ b/src/components/common/modal/children/TripSurveyMember.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ParticipantStatus } from '@components/Trip/TripParticipant'; +import Tab from '@components/common/tab/Tab'; + +const TripSurveyMember: React.FC = () => { + return ( +
+ , + , + ]} + /> +
+ ); +}; + +export default TripSurveyMember; diff --git a/src/components/common/nav/InputComment.tsx b/src/components/common/nav/InputComment.tsx index b186b9d4..3e3bb64d 100644 --- a/src/components/common/nav/InputComment.tsx +++ b/src/components/common/nav/InputComment.tsx @@ -106,8 +106,8 @@ export const InputComment: React.FC = () => { }; return ( -
-
+
+
= () => { onKeyPress={handleKeyPress} />
-
+