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/assets/images/FifthMarker.png b/src/assets/images/FifthMarker.png new file mode 100644 index 00000000..617795ce Binary files /dev/null and b/src/assets/images/FifthMarker.png differ diff --git a/src/assets/images/FifthSelectedMarker.png b/src/assets/images/FifthSelectedMarker.png new file mode 100644 index 00000000..b931eb7b Binary files /dev/null and b/src/assets/images/FifthSelectedMarker.png differ diff --git a/src/assets/images/FirstMarker.png b/src/assets/images/FirstMarker.png new file mode 100644 index 00000000..b317d65c Binary files /dev/null and b/src/assets/images/FirstMarker.png differ diff --git a/src/assets/images/FirstSelectedMarker.png b/src/assets/images/FirstSelectedMarker.png new file mode 100644 index 00000000..632130de Binary files /dev/null and b/src/assets/images/FirstSelectedMarker.png differ diff --git a/src/assets/images/FourthMarker.png b/src/assets/images/FourthMarker.png new file mode 100644 index 00000000..f1963795 Binary files /dev/null and b/src/assets/images/FourthMarker.png differ diff --git a/src/assets/images/FourthSelectedMarker.png b/src/assets/images/FourthSelectedMarker.png new file mode 100644 index 00000000..16fcbf5b Binary files /dev/null and b/src/assets/images/FourthSelectedMarker.png differ diff --git a/src/assets/images/SecondMarker.png b/src/assets/images/SecondMarker.png new file mode 100644 index 00000000..741e7cdb Binary files /dev/null and b/src/assets/images/SecondMarker.png differ diff --git a/src/assets/images/SecondSelectedMarker.png b/src/assets/images/SecondSelectedMarker.png new file mode 100644 index 00000000..d236dc12 Binary files /dev/null and b/src/assets/images/SecondSelectedMarker.png differ diff --git a/src/assets/images/ThirdMarker.png b/src/assets/images/ThirdMarker.png new file mode 100644 index 00000000..a55fa7fc Binary files /dev/null and b/src/assets/images/ThirdMarker.png differ diff --git a/src/assets/images/ThirdSelectedMarker.png b/src/assets/images/ThirdSelectedMarker.png new file mode 100644 index 00000000..bdfe7cb3 Binary files /dev/null and b/src/assets/images/ThirdSelectedMarker.png differ 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/Plan/TripBudget.tsx b/src/components/Plan/TripBudget.tsx index 8838fb25..bac3e435 100644 --- a/src/components/Plan/TripBudget.tsx +++ b/src/components/Plan/TripBudget.tsx @@ -1,3 +1,4 @@ +import Alert from '@components/common/alert/Alert'; import { SettingIcons } from '@components/common/icons/Icons'; import { socketContext } from '@hooks/useSocket'; import * as Progress from '@radix-ui/react-progress'; @@ -13,7 +14,9 @@ const TripBudget = () => { useEffect(() => { if (budget) { setTargetBudget(budget.budget || 0); - setCurrentSpending(budget.calculatedPrice || 0); + setCurrentSpending( + budget.calculatedPrice >= 0 ? budget.calculatedPrice : 0, + ); } }, [budget]); @@ -25,52 +28,76 @@ const TripBudget = () => { 100, ); - // 목표 경비 설정 함수 - const handleSetTargetBudget = (newTargetBudget: number) => { - setTargetBudget(newTargetBudget); - }; + // // 목표 경비 설정 함수 + // const handleSetTargetBudget = (newTargetBudget: number) => { + // setTargetBudget(newTargetBudget); + // }; return ( -
-
사용 경비
-
- - {budget?.calculatedPrice.toLocaleString()} - - -
+ <> +
+
사용 경비
+
+ + {currentSpending.toLocaleString()} + + +
- - = 100 ? 'bg-sub2' : 'bg-main2' - } transition-transform duration-[660ms]`} + = 100 ? 0 : -100 + progress}%)`, + transform: 'translateZ(0)', }} - /> - + value={progress}> + = 100 ? 'bg-sub2' : 'bg-main2' + } transition-transform duration-[660ms]`} + style={{ + transform: `translateX(${ + progress >= 100 ? 0 : -100 + progress + }%)`, + }} + /> + -
-
- 목표 경비 - -
-
- {budget?.budget.toLocaleString()} - +
+
+ 목표 경비 + { + console.log('확인'); + }} + onCancel={() => { + console.log('취소'); + }} + children={ + + } + content={ +
+ + +
+ } + /> +
+
+ {targetBudget.toLocaleString()} + +
-
+ ); }; diff --git a/src/components/Plan/TripMap.tsx b/src/components/Plan/TripMap.tsx index 7c7cc026..9816403e 100644 --- a/src/components/Plan/TripMap.tsx +++ b/src/components/Plan/TripMap.tsx @@ -1,6 +1,16 @@ import { Paths } from '@/@types/service'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Map, MapMarker, Polyline, useKakaoLoader } from 'react-kakao-maps-sdk'; +import FirstMarker from '@/assets/images/FirstMarker.png'; +import FirstSelectedMarker from '@/assets/images/FirstSelectedMarker.png'; +import SecondMarker from '@/assets/images/SecondMarker.png'; +import ThirdMarker from '@/assets/images/ThirdMarker.png'; +import FourthMarker from '@/assets/images/FourthMarker.png'; +import FifthMarker from '@/assets/images/FifthMarker.png'; +import SecondSelectedMarker from '@/assets/images/SecondSelectedMarker.png'; +import ThirdSelectedMarker from '@/assets/images/ThirdSelectedMarker.png'; +import FourthSelectedMarker from '@/assets/images/FourthSelectedMarker.png'; +import FifthSelectedMarker from '@/assets/images/FifthSelectedMarker.png'; const VITE_KAKAO_MAP_API_KEY = import.meta.env.VITE_KAKAO_MAP_API_KEY; @@ -87,6 +97,44 @@ const TripMap = ({ paths }: { paths: Paths[] }) => { setBounds(); }, [paths]); + // 선택된 마커의 인덱스를 추적하기 위한 상태 + const [selectedMarker, setSelectedMarker] = useState(null); + + // ... + + // 마커를 클릭할 때 호출되는 함수 + const handleMarkerClick = (index: number) => { + setSelectedMarker(index); + }; + + // 각 마커에 대한 이미지를 렌더링하는 함수 + const renderMarkerImage = (index: number, isSelected: boolean) => { + let svgComponent; + switch (index % 5) { + case 0: + svgComponent = isSelected ? FirstSelectedMarker : FirstMarker; + break; + case 1: + svgComponent = isSelected ? SecondSelectedMarker : SecondMarker; + break; + case 2: + svgComponent = isSelected ? ThirdSelectedMarker : ThirdMarker; + break; + case 3: + svgComponent = isSelected ? FourthSelectedMarker : FourthMarker; + break; + case 4: + svgComponent = isSelected ? FifthSelectedMarker : FifthMarker; + break; + default: + // 기본 마커가 필요한 경우 기본 마커 이미지 URL을 제공합니다. + return 'default_marker_image_url'; + } + return svgComponent; + }; + + // ... TripMap 컴포넌트 및 나머지 코드 + return (
{ lat: Number(path.fromLatitude), lng: Number(path.fromLongitude), }} + onClick={() => handleMarkerClick(index)} + image={{ + src: renderMarkerImage(index, selectedMarker === index), + size: { + width: 33, + height: 33, + }, + }} /> handleMarkerClick(index)} + image={{ + src: renderMarkerImage(index, selectedMarker === index), + size: { + width: 33, + height: 33, + }, + }} /> void) | ((e: React.MouseEvent) => void); onCancel?: (() => void) | ((e: React.MouseEvent) => void); children: ReactNode; + content?: ReactNode; + closeOnConfirm?: boolean; + isCheck?: number | null; } const Alert: FC = ({ @@ -15,17 +18,33 @@ const Alert: FC = ({ onConfirm, onCancel, children, + content, + closeOnConfirm = false, + isCheck = true, }) => ( - {children} - + + {children} + - - -

{title}

+ + +

+ {title} +

{message}

+ {content}
@@ -35,11 +54,21 @@ const Alert: FC = ({ 취소 - + {closeOnConfirm ? ( + + + + ) : ( + + )}
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/toastpopup/ToastPopUp.tsx b/src/components/common/toastpopup/ToastPopUp.tsx index e57ab098..1abfa1d9 100644 --- a/src/components/common/toastpopup/ToastPopUp.tsx +++ b/src/components/common/toastpopup/ToastPopUp.tsx @@ -21,9 +21,11 @@ const ToastPopUp: React.FC = ({ noun, verb }) => { return () => clearTimeout(timeout); }, []); - if (noun === '일정') { - setParticle('이'); - } + useEffect(() => { + if (noun === '일정' || noun === '날짜 이동') { + setParticle('이'); + } + }, [noun]); return (
= ({ noun, verb }) => { transition: 'transform 0.5s ease-in-out, opacity 0.5s ease-in-out', opacity: visible ? 1 : 0, zIndex: 1, // 이거 해줘야 kakao-map도 dimmed됨 + marginTop: '56px', }}>

{noun} - {particle} {verb}되었습니다 + {particle} {verb}되었습니다.

); diff --git a/src/recoil/plan.ts b/src/recoil/plan.ts new file mode 100644 index 00000000..c938efaa --- /dev/null +++ b/src/recoil/plan.ts @@ -0,0 +1,11 @@ +import { atom } from 'recoil'; + +export const dayState = atom({ + key: 'dayState', + default: [''], +}); + +export const dateState = atom({ + key: 'dateState', + default: [''], +}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 35bb10df..b371a609 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -35,7 +35,7 @@ export function calculateDayAndDate(startDate: string, endDate: string) { const DayArr = Array.from( { length: Math.ceil(differenceInDays) + 1 }, - (_, i) => `Day${i + 1}`, + (_, i) => `DAY ${i + 1}`, ); const DateArr = [];