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일)
-
+
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}>
+
+
+
+
+
+
+
+ {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) => (
- <>
-
-
-
-
-
- {item.category}
-
-
- {item.price} 원
+
+
+ {index !== 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {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) => (
+
+ handleMoveItem(date[index])}
+ className="relative flex flex-shrink-0 flex-grow-0 justify-start gap-2">
+
+
+ {day}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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 &&
}
+
+
+
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 (
- 내 여행 취향 설정하러 가기
+
+
+
+
+
내 여행 취향 설정하러 가기
+
@@ -87,13 +94,25 @@ const Percentage = ({ value, total, color }: PercentageParams) => (
);
const TripPreference: React.FC = () => {
- const params = useParams();
- const tripId = Number(params.id);
const [A, setA] = useState<[number, number]>([0, 0]);
const [B, setB] = useState<[number, number]>([0, 0]);
const [C, setC] = useState<[number, number]>([0, 0]);
const [D, setD] = useState<[number, number]>([0, 0]);
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),
+ });
+
+ useEffect(() => {
+ const participants = tripsSurveyMembers?.data?.data;
+ setParticipants(participants);
+ }, [tripsSurveyMembers]);
const { data: tripPreference, isLoading } = useQuery({
queryKey: ['tripPreference', tripId],
@@ -129,11 +148,20 @@ const TripPreference: React.FC = () => {
return Loading...
;
}
+ const handleButtonClick = () => {
+ setModalChildren('TripSurveyMember');
+ setIsModalOpen(true);
+ };
+
return (
-
+
-
-
n명 참여
+
+
+ {participants?.tripSurveyMemberCount}명 참여
+
diff --git a/src/components/common/accordion/Accordion.tsx b/src/components/common/accordion/Accordion.tsx
index 6bcf4b90..4175b9f7 100644
--- a/src/components/common/accordion/Accordion.tsx
+++ b/src/components/common/accordion/Accordion.tsx
@@ -12,8 +12,8 @@ const Accordion: React.FC
= ({ title, content }) => {
{title}
-
-
+
+
{content}
diff --git a/src/components/common/alert/Alert.tsx b/src/components/common/alert/Alert.tsx
index 0d1686b1..6aaad0c7 100644
--- a/src/components/common/alert/Alert.tsx
+++ b/src/components/common/alert/Alert.tsx
@@ -7,6 +7,8 @@ interface AlertProps {
onConfirm: (() => void) | ((e: React.MouseEvent) => void);
onCancel?: (() => void) | ((e: React.MouseEvent) => void);
children: ReactNode;
+ closeOnConfirm?: boolean;
+ isCheck?: number | null;
}
const Alert: FC = ({
@@ -15,10 +17,13 @@ const Alert: FC = ({
onConfirm,
onCancel,
children,
+ closeOnConfirm = false,
+ isCheck = true,
}) => (
- {children}
-
+
+ {children}
+
@@ -35,11 +40,21 @@ const Alert: FC = ({
취소
-
- 확인
-
+ {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 (
+
+ {children}
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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}
/>
-
+
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/pages/plan/OurLikedList.tsx b/src/pages/plan/OurLikedList.tsx
new file mode 100644
index 00000000..bb192296
--- /dev/null
+++ b/src/pages/plan/OurLikedList.tsx
@@ -0,0 +1,100 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import ToursCategoryItem from '@components/Tours/ToursCategoryItem';
+import { useEffect, useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { getMemberTours } from '@api/member';
+
+export const OurLikedList = () => {
+ const categories = ['전체', '숙소', '식당', '관광지'];
+
+ // const [selectedContentTypeId, setSelectedContentTypeId] = useState<
+ // null | number
+ // >(null);
+
+ const [selectedCategory, setSelectedCategory] = useState('전체');
+ useEffect(() => {
+ console.log(selectedCategory);
+ }, [selectedCategory]);
+
+ const handleSelectCategory = (category: string) => {
+ setSelectedCategory(category);
+ };
+
+ // useEffect(() => {
+ // console.log('searchWord: ' + searchWord);
+ // }, [searchWord]);
+ // console.log();
+
+ const {
+ // fetchNextPage, hasNextPage,
+ data,
+ isLoading,
+ isError,
+ } = useInfiniteQuery({
+ queryKey: ['wishList'],
+ queryFn: ({ pageParam = 0 }) => getMemberTours(pageParam, 10),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) => {
+ if (
+ lastPage &&
+ lastPage.data &&
+ lastPage.data &&
+ lastPage.data.pageable
+ ) {
+ const currentPage = lastPage.data.pageable.pageNumber;
+ const totalPages = lastPage.data.totalPages;
+
+ if (currentPage < totalPages - 1) {
+ return currentPage + 1;
+ }
+ }
+ return undefined;
+ },
+ });
+
+ // const handleCategoryClick = (contentTypeId: number | null) => {
+ // setSelectedContentTypeId(contentTypeId);
+ // };
+
+ if (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ // console.log(data?.pages[0].data.content);
+ const searchResults = data?.pages.flatMap((page) => page.data.content) || [];
+ console.log('searchResults', searchResults);
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+ 우리의 관심 목록
+
+ {categories.map((category) => (
+
+ ))}
+
+ {noResults ? (
+
+ 나의 관심목록이 없습니다.
+
+ ) : (
+
+ //
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/AddtoListBtn.tsx b/src/pages/plan/addPlace/AddtoListBtn.tsx
new file mode 100644
index 00000000..9e54f742
--- /dev/null
+++ b/src/pages/plan/addPlace/AddtoListBtn.tsx
@@ -0,0 +1,36 @@
+import { useRecoilValue } from 'recoil';
+import { selectedItemsState } from '@recoil/listItem';
+import { ButtonPrimary } from '@components/common/button/Button';
+import { postTripsLike } from '@api/trips';
+// import { useNavigate } from 'react-router-dom';
+
+const AddToListButton = () => {
+ const selectedTourItemIds = useRecoilValue(selectedItemsState);
+ // const navigate = useNavigate();
+
+ const getTripIdFromUrl = () => {
+ const pathSegments = window.location.pathname.split('/');
+ const tripIdIndex =
+ pathSegments.findIndex((segment) => segment === 'trip') + 1;
+ return pathSegments[tripIdIndex]
+ ? parseInt(pathSegments[tripIdIndex], 10)
+ : null;
+ };
+
+ const handleAddClick = async () => {
+ const tripId = getTripIdFromUrl();
+ if (tripId) {
+ try {
+ const response = await postTripsLike(tripId, selectedTourItemIds);
+ console.log('API response:', response);
+ // navigate(`/trip/${tripId}`);
+ } catch (error) {
+ console.error('API error:', error);
+ }
+ }
+ };
+
+ return 추가하기;
+};
+
+export default AddToListButton;
diff --git a/src/pages/plan/addPlace/MyLiked.tsx b/src/pages/plan/addPlace/MyLiked.tsx
new file mode 100644
index 00000000..5e65a8d3
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLiked.tsx
@@ -0,0 +1,73 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { getMemberTours } from '@api/member';
+import { MyLikedList } from './MyLikedList';
+import WishCategory from '@components/Wish/WishCategory';
+import AddToListButton from './AddtoListBtn';
+
+export const MyLiked = () => {
+ const [selectedContentTypeId, setSelectedContentTypeId] = useState<
+ null | number
+ >(null);
+
+ const handleCategoryClick = (contentTypeId: number | null) => {
+ setSelectedContentTypeId(contentTypeId);
+ };
+
+ const { fetchNextPage, hasNextPage, data, isLoading, isError } =
+ useInfiniteQuery({
+ queryKey: ['wishList'],
+ queryFn: ({ pageParam = 0 }) => getMemberTours(pageParam, 10),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage) => {
+ if (
+ lastPage &&
+ lastPage.data &&
+ lastPage.data &&
+ lastPage.data.pageable
+ ) {
+ const currentPage = lastPage.data.pageable.pageNumber;
+ const totalPages = lastPage.data.totalPages;
+
+ if (currentPage < totalPages - 1) {
+ return currentPage + 1;
+ }
+ }
+ return undefined;
+ },
+ });
+
+ if (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ const searchResults = data?.pages.flatMap((page) => page.data.content) || [];
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+ 나의 관심 목록
+
+ {noResults ? (
+
+ 나의 관심목록이 없습니다.
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/MyLikedList.tsx b/src/pages/plan/addPlace/MyLikedList.tsx
new file mode 100644
index 00000000..295328c6
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLikedList.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import InfiniteScroll from 'react-infinite-scroller';
+import { v4 as uuidv4 } from 'uuid';
+import { MyLikedListItem } from './MyLikedListItem';
+import { TourType } from '@/@types/tours.types';
+import { Spinner } from '@components/common/spinner/Spinner';
+
+interface WishListProps {
+ toursData: { pages: Array<{ data: { content: TourType[] } }> };
+ fetchNextPage: () => void;
+ hasNextPage: boolean;
+ isLoading: boolean;
+ selectedContentTypeId: number | null;
+}
+
+export const MyLikedList: React.FC = ({
+ toursData,
+ fetchNextPage,
+ hasNextPage,
+ isLoading,
+ selectedContentTypeId,
+}) => {
+ if (!toursData || toursData.pages.length === 0) {
+ return 데이터를 불러오는 중 오류가 발생했습니다.
;
+ }
+
+ const filteredData =
+ selectedContentTypeId !== null
+ ? toursData.pages.map((group) => ({
+ data: {
+ content: group.data.content.filter(
+ (item) => item.contentTypeId === selectedContentTypeId,
+ ),
+ },
+ }))
+ : toursData.pages;
+
+ return (
+
+
fetchNextPage()}
+ hasMore={hasNextPage}
+ loader={
+
+
+
+ }>
+
+ {!isLoading &&
+ filteredData.map((group) => (
+
+ {group?.data.content.map((wishList: TourType) => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/pages/plan/addPlace/MyLikedListItem.tsx b/src/pages/plan/addPlace/MyLikedListItem.tsx
new file mode 100644
index 00000000..c1cdcfa7
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLikedListItem.tsx
@@ -0,0 +1,57 @@
+import { TourType } from '@/@types/tours.types';
+import { ListCheckBtn } from '@components/common/button/ListSelectBtn';
+import { StarIcon } from '@components/common/icons/Icons';
+import { selectedItemsState } from '@recoil/listItem';
+import { useRecoilState } from 'recoil';
+
+interface WishItemProps {
+ wishList: TourType;
+}
+
+export const MyLikedListItem: React.FC = ({ wishList }) => {
+ const {
+ id,
+ title,
+ ratingAverage,
+ reviewCount,
+ smallThumbnailUrl,
+ tourAddress,
+ } = wishList;
+
+ const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsState);
+
+ const handleSelect = () => {
+ if (selectedItems.includes(id)) {
+ setSelectedItems(selectedItems.filter((item) => item !== id));
+ } else {
+ setSelectedItems([...selectedItems, id]);
+ }
+ };
+
+ return (
+
+
+
+
+
+
{title}
+
+
+
+
+
+ {ratingAverage}({reviewCount})
+
+
+ {tourAddress}
+
+
+
+
+
+ );
+};
diff --git a/src/pages/plan/addPlace/PlanAddPlace.page.tsx b/src/pages/plan/addPlace/PlanAddPlace.page.tsx
new file mode 100644
index 00000000..98002a06
--- /dev/null
+++ b/src/pages/plan/addPlace/PlanAddPlace.page.tsx
@@ -0,0 +1,32 @@
+import SearchInput from '@components/search/SearchInput';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { SearchResultForPlan } from './SearchResult';
+import { OurLikedList } from '../OurLikedList';
+
+export const PlanAddPlace = () => {
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+ const searchWordFromQuery = queryParams.get('searchWord');
+
+ const [searchWord, setSearchWord] = useState('');
+
+ useEffect(() => {
+ if (searchWordFromQuery) {
+ setSearchWord(searchWordFromQuery);
+ } else {
+ setSearchWord('');
+ }
+ }, [location, searchWordFromQuery]);
+ return (
+ <>
+
+ {searchWord ? (
+
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/ResultCategoryPlan.tsx b/src/pages/plan/addPlace/ResultCategoryPlan.tsx
new file mode 100644
index 00000000..7e3f8706
--- /dev/null
+++ b/src/pages/plan/addPlace/ResultCategoryPlan.tsx
@@ -0,0 +1,47 @@
+import { ButtonWhite } from '@components/common/button/Button';
+import { TourType } from '@/@types/tours.types';
+import { InfiniteQueryObserverResult } from '@tanstack/react-query';
+import { ResultItemPlan } from './ResultItem';
+import AddToListButton from './AddtoListBtn';
+
+interface ResultCategoryProps {
+ data: TourType[];
+ category: string;
+ fetchNextPage: (() => Promise>) | null;
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+}
+
+export const ResultCategoryPlan = ({
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+}: ResultCategoryProps) => {
+ return (
+ <>
+ ResultCategoryPlan
+ {data.map((item) => (
+
+ ))}
+ {hasNextPage && !isFetchingNextPage ? (
+ fetchNextPage && fetchNextPage()}>
+ 더보기
+
+ ) : isFetchingNextPage ? (
+
+ Loading...
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/ResultItem.tsx b/src/pages/plan/addPlace/ResultItem.tsx
new file mode 100644
index 00000000..9ad70dde
--- /dev/null
+++ b/src/pages/plan/addPlace/ResultItem.tsx
@@ -0,0 +1,43 @@
+import { TourType } from '@/@types/tours.types';
+import { ListSelectBtn } from '@components/common/button/ListSelectBtn';
+import { StarIcon } from '@components/common/icons/Icons';
+import { selectedItemsState } from '@recoil/listItem';
+import { useRecoilState } from 'recoil';
+
+export const ResultItemPlan = ({ result }: { result: TourType }) => {
+ const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsState);
+ const id = result.id;
+ const handleSelect = () => {
+ if (selectedItems.includes(id)) {
+ setSelectedItems(selectedItems.filter((item) => item !== id));
+ } else {
+ setSelectedItems([...selectedItems, id]);
+ }
+ };
+ return (
+
+
+
+
+
+
{result.title}
+
+
+
+
+
+ {result.ratingAverage}({result.reviewCount})
+
+
+ {result.tourAddress}
+
+
+
+
선택
+
+ );
+};
diff --git a/src/pages/plan/addPlace/SearchResult.tsx b/src/pages/plan/addPlace/SearchResult.tsx
new file mode 100644
index 00000000..485f37e8
--- /dev/null
+++ b/src/pages/plan/addPlace/SearchResult.tsx
@@ -0,0 +1,94 @@
+import { getToursSearch } from '@api/tours';
+import { useInfiniteQuery } from '@tanstack/react-query';
+import ToursCategoryItem from '@components/Tours/ToursCategoryItem';
+import { useEffect, useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { ResultCategoryPlan } from './ResultCategoryPlan';
+
+interface SearchResultProps {
+ searchWord: string;
+}
+
+export const SearchResultForPlan = ({ searchWord }: SearchResultProps) => {
+ const categories = ['전체', '숙소', '식당', '관광지'];
+ const [selectedCategory, setSelectedCategory] = useState('전체');
+ useEffect(() => {
+ console.log(selectedCategory);
+ }, [selectedCategory]);
+
+ const handleSelectCategory = (category: string) => {
+ setSelectedCategory(category);
+ };
+
+ useEffect(() => {
+ console.log('searchWord: ' + searchWord);
+ }, [searchWord]);
+ console.log();
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isLoading,
+ isError,
+ isFetchingNextPage,
+ } = useInfiniteQuery({
+ queryKey: ['searchResults', searchWord, selectedCategory],
+ queryFn: ({ pageParam = 0 }) =>
+ getToursSearch({
+ region: '',
+ searchWord: searchWord,
+ category: selectedCategory !== '전체' ? selectedCategory : undefined,
+ page: pageParam,
+ size: 20,
+ }),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage, allPages) => {
+ if (!lastPage.data.data.last) {
+ return allPages.length;
+ }
+ return undefined;
+ },
+ enabled: !!searchWord,
+ retry: 2,
+ });
+
+ if (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ const searchResults =
+ data?.pages.flatMap((page) => page.data.data.content) || [];
+ console.log('searchResults', searchResults);
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+
+ {categories.map((category) => (
+
+ ))}
+
+
+ {noResults ? (
+ 검색결과가 없습니다.
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/planPlaceTrip.page.tsx b/src/pages/plan/planPlaceTrip.page.tsx
deleted file mode 100644
index 4ff978c4..00000000
--- a/src/pages/plan/planPlaceTrip.page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const PlanPlaceTrip = () => {
- return 여행계획 - 장소 추가 페이지
;
-};
-
-export default PlanPlaceTrip;
diff --git a/src/pages/trip/AddOurList.tsx b/src/pages/trip/AddOurList.tsx
new file mode 100644
index 00000000..80bce838
--- /dev/null
+++ b/src/pages/trip/AddOurList.tsx
@@ -0,0 +1,32 @@
+import SearchInput from '@components/search/SearchInput';
+import { MyLiked } from '@pages/plan/addPlace/MyLiked';
+import { SearchResultForPlan } from '@pages/plan/addPlace/SearchResult';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export const AddOurList = () => {
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+ const searchWordFromQuery = queryParams.get('searchWord');
+
+ const [searchWord, setSearchWord] = useState('');
+
+ useEffect(() => {
+ if (searchWordFromQuery) {
+ setSearchWord(searchWordFromQuery);
+ } else {
+ setSearchWord('');
+ }
+ }, [location, searchWordFromQuery]);
+ return (
+
+
+ {searchWord ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/src/recoil/listItem.ts b/src/recoil/listItem.ts
new file mode 100644
index 00000000..72eb7456
--- /dev/null
+++ b/src/recoil/listItem.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const selectedItemsState = atom({
+ key: 'selectedItemsState',
+ default: [],
+});
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/recoil/trip.ts b/src/recoil/trip.ts
new file mode 100644
index 00000000..9df993e0
--- /dev/null
+++ b/src/recoil/trip.ts
@@ -0,0 +1,22 @@
+import { atom } from 'recoil';
+
+type Participant = {
+ memberId: number;
+ nickname: string;
+ thumbnail: string;
+};
+
+export type Participants = {
+ tripSurveyMemberCount: number;
+ tripSurveySetMemberInfos: Participant[];
+ nonTripSurveySetMemberInfos: Participant[];
+};
+
+export const participantsState = atom({
+ key: 'participantsState',
+ default: {
+ tripSurveyMemberCount: 0,
+ tripSurveySetMemberInfos: [],
+ nonTripSurveySetMemberInfos: [],
+ },
+});
diff --git a/src/router/socketRouter.tsx b/src/router/socketRouter.tsx
index 4503f830..a48c16dc 100644
--- a/src/router/socketRouter.tsx
+++ b/src/router/socketRouter.tsx
@@ -1,12 +1,13 @@
import { Route, Routes } from 'react-router-dom';
import { useSocket, socketContext } from '@hooks/useSocket';
import PlanTrip from '@pages/plan/planTrip.page';
-import PlanPlaceTrip from '@pages/plan/planPlaceTrip.page';
+import { PlanAddPlace } from '@pages/plan/addPlace/PlanAddPlace.page';
import PlanPlaceSearch from '@pages/plan/planPlaceSearch.page';
import Trip from '@pages/trip/trip.page';
import MainLayout from './routerLayout';
import { useRecoilValue } from 'recoil';
import { tripIdState, visitDateState } from '@recoil/socket';
+import { AddOurList } from '@pages/trip/AddOurList';
const SocketRoutes = () => {
const tripId = useRecoilValue(tripIdState);
@@ -19,7 +20,7 @@ const SocketRoutes = () => {
} />
- } />
+ } />
} />
@@ -31,6 +32,7 @@ const SocketRouter = () => {
}>
} />
+ } />
} />
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 = [];
diff --git a/tailwind.config.js b/tailwind.config.js
index 0ec3313f..1853d03c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -23,4 +23,9 @@ export default {
},
},
plugins: [require('tailwind-scrollbar-hide')],
+ safelist: [
+ {
+ pattern: /(bg|text|border|sub2|orange|purple|green)-/,
+ },
+ ],
};