Skip to content

Commit 04574e4

Browse files
authored
Merge pull request #965 from sparcs-kaist/952-luggage-identifier
952 luggage identifier
2 parents ccf488a + cb4d428 commit 04574e4

10 files changed

Lines changed: 352 additions & 41 deletions

File tree

‎packages/web/src/components/Chat/Header/SideMenu.tsx‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { memo, useCallback, useEffect, useState } from "react";
1+
import { memo, useCallback, useEffect, useMemo, useState } from "react";
22

3+
import { useValueRecoilState } from "@/hooks/useFetchRecoilState";
34
import useIsTimeOver from "@/hooks/useIsTimeOver";
45
import { useAxios } from "@/hooks/useTaxiAPI";
56

@@ -10,6 +11,7 @@ import {
1011
ModalChatReport,
1112
ModalRoomShare,
1213
} from "@/components/ModalPopup";
14+
import CarrierOptionRow from "@/components/ModalRoomOptions/CarrierOptionRow";
1315
import User from "@/components/User";
1416

1517
import alertAtom from "@/atoms/alert";
@@ -77,13 +79,42 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => {
7779
const axios = useAxios();
7880

7981
const setAlert = useSetRecoilState(alertAtom);
82+
const loginInfo = useValueRecoilState("loginInfo");
8083
const [isOpenShare, setIsOpenShare] = useState<boolean>(false);
8184
const [isOpenCallTaxi, setIsOpenCallTaxi] = useState<boolean>(false);
8285
const [isOpenReport, setIsOpenReport] = useState<boolean>(false);
8386
const [isOpenCancel, setIsOpenCancel] = useState<boolean>(false);
8487
const isDeparted = useIsTimeOver(dayServerToClient(roomInfo.time)); // 방 출발 여부
88+
const [localPart, setLocalPart] = useState<User[]>(roomInfo.part);
8589

86-
const onClikcShare = useCallback(() => setIsOpenShare(true), []);
90+
const myCarrierStatus = useMemo(() => {
91+
return localPart.find((u) => u._id === loginInfo?.oid)?.hasCarrier || false;
92+
}, [localPart, loginInfo]);
93+
94+
const handleToggleCarrier = async () => {
95+
const nextStatus = !myCarrierStatus;
96+
97+
try {
98+
await axios({
99+
url: "/rooms/carrier/toggle",
100+
method: "post",
101+
data: {
102+
roomId: roomInfo._id,
103+
hasCarrier: nextStatus,
104+
},
105+
onSuccess: (data) => {
106+
if (data && data.part) {
107+
setLocalPart(data.part);
108+
}
109+
},
110+
onError: () => setAlert("캐리어 상태 변경에 실패했습니다."),
111+
});
112+
} catch (e) {
113+
console.error(e);
114+
}
115+
};
116+
117+
const onClickShare = useCallback(() => setIsOpenShare(true), []);
87118
const onClickCancel = useCallback(
88119
() =>
89120
isDeparted
@@ -200,17 +231,27 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => {
200231
<PeopleAltRoundedIcon style={styleIcon} />
201232
<div css={{ ...styleInfo }}>
202233
참여 / 최대 인원 :{" "}
203-
<span css={theme.font14_bold}>{roomInfo.part.length}명</span>{" "}
234+
<span css={theme.font14_bold}>{localPart.length}명</span>{" "}
204235
<span css={{ color: theme.gray_text }}>
205236
/ {roomInfo.maxPartLength}명
206237
</span>
207238
</div>
208239
</div>
240+
209241
<div css={styleUsers}>
210-
{roomInfo.part.map((item) => (
242+
{localPart.map((item) => (
211243
<User key={item._id} value={item} isDeparted={isDeparted} />
212244
))}
213245
</div>
246+
247+
{!isDeparted && (
248+
<div css={{ marginTop: "12px" }}>
249+
<CarrierOptionRow
250+
value={myCarrierStatus}
251+
handler={handleToggleCarrier}
252+
/>
253+
</div>
254+
)}
214255
</div>
215256
<DottedLine />
216257
{taxiFare !== 0 ? (
@@ -226,7 +267,7 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => {
226267
<DottedLine />
227268
</>
228269
) : null}
229-
<SideMenuButton type="share" onClick={onClikcShare} />
270+
<SideMenuButton type="share" onClick={onClickShare} />
230271
<DottedLine />
231272
<SideMenuButton type="taxi" onClick={onClickCallTaxi} />
232273
{!isAlone && (

‎packages/web/src/components/ModalPopup/Body/BodyRoomSelection.tsx‎

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Button from "@/components/Button";
1414
import DottedLine from "@/components/DottedLine";
1515
import LinkLogin from "@/components/Link/LinkLogin";
1616
import MiniCircle from "@/components/MiniCircle";
17+
import CarrierOptionRow from "@/components/ModalRoomOptions/CarrierOptionRow";
1718
import Users from "@/components/User/Users";
1819
import { MAX_PARTICIPATION } from "@/pages/Myroom";
1920

@@ -37,8 +38,10 @@ type InfoSectionProps = {
3738
alignDirection: "left" | "right";
3839
children: React.ReactNode;
3940
};
41+
4042
export type BodyRoomSelectionProps = {
4143
roomInfo: Room;
44+
onToggleCarrier?: () => void;
4245
triggerTags?: string;
4346
};
4447

@@ -116,44 +119,75 @@ const BodyRoomSelection = ({
116119
const fetchMyRooms = useFetchRecoilState("myRooms");
117120
const setAlert = useSetRecoilState(alertAtom);
118121

119-
const isLogin = useIsLogin() && !!loginInfo?.id; // 로그인 여부
120-
const isRoomFull = roomInfo && roomInfo.part.length >= roomInfo.maxPartLength; // 방이 꽉 찼는지 여부
122+
// 참여 시 캐리어 지참 여부 상태 (기본값 false)
123+
const [joinWithCarrier, setJoinWithCarrier] = useState(false);
124+
125+
const isLogin = useIsLogin() && !!loginInfo?.id;
126+
const isRoomFull = roomInfo && roomInfo.part.length >= roomInfo.maxPartLength;
127+
121128
const isAlreadyPart =
122129
isLogin &&
123130
roomInfo &&
124131
(roomInfo.part.some(
125132
(user: Room["part"][number]) => user._id === loginInfo.oid
126133
) ??
127-
true); // 이미 참여 중인지 여부
134+
true);
135+
128136
const isMaxPart =
129-
isLogin && myRooms && myRooms.ongoing.length >= MAX_PARTICIPATION; // 최대 참여 가능한 방 개수를 초과했는지 여부
130-
const isDepart = useIsTimeOver(dayServerToClient(roomInfo.time)); // 방 출발 여부
137+
isLogin && myRooms && myRooms.ongoing.length >= MAX_PARTICIPATION;
138+
const isDepart = useIsTimeOver(dayServerToClient(roomInfo.time));
131139

132140
const notPaid = useMemo(() => {
133141
const myOngoingRoom = myRooms?.ongoing.slice() ?? [];
134142
const notPaid = myOngoingRoom.find(
135143
(room) =>
136144
room.part.find((item: any) => item._id === loginInfo?.oid)
137-
.isSettlement === "send-required" && room.isDeparted
138-
); // 다른 사람이 정산을 올렸으나 내가 아직 송금하지 않은 방이 있는지 여부 (추가 입장 제한에 사용)
145+
?.isSettlement === "send-required" && room.isDeparted
146+
);
139147
return notPaid;
140-
}, [myRooms]); // myOngoingRoom은 infoSection의 sortedMyRoom에서 정렬만 뺀 코드입니다. useMemo로 감싼 형태입니다.
141-
// item : any 가 좋은 방법인지 모르겠습니다
148+
}, [myRooms, loginInfo]);
149+
150+
const participantsWithCarrier = useMemo(() => {
151+
if (!roomInfo?.part) return [];
152+
return roomInfo.part.map((user: any) => ({
153+
...user,
154+
withCarrier: user.hasCarrier || false,
155+
}));
156+
}, [roomInfo.part]);
142157

143158
const requestJoin = useCallback(async () => {
159+
// 이미 참여 중이면 이동만
144160
if (isAlreadyPart) {
145-
// 이미 참여 중인 방에서 버튼을 누르면 API 호출 관련 로직을 건너뛰고 해당 방으로 이동합니다.
146161
history.push(`/myroom/${roomInfo._id}`);
147162
return;
148163
}
149-
// 여기부터는 이미 참여 중인 방이 아닌 경우의 로직입니다.
164+
150165
if (onCall.current) return;
151166
onCall.current = true;
167+
152168
await axios({
153169
url: "/rooms/join",
154170
method: "post",
155171
data: { roomId: roomInfo._id },
156-
onSuccess: () => {
172+
onSuccess: async () => {
173+
// 만약 캐리어를 들고 탄다고 체크했다면 토글 API 추가 호출
174+
if (joinWithCarrier) {
175+
try {
176+
await axios({
177+
url: "/rooms/carrier/toggle",
178+
method: "post",
179+
data: {
180+
roomId: roomInfo._id,
181+
hasCarrier: true,
182+
},
183+
// ...
184+
});
185+
} catch (e) {
186+
console.error("캐리어 상태 반영 실패", e);
187+
}
188+
}
189+
190+
// 내 방 목록 갱신 및 페이지 이동
157191
fetchMyRooms();
158192
history.push(`/myroom/${roomInfo._id}`);
159193

@@ -163,10 +197,12 @@ const BodyRoomSelection = ({
163197
}
164198
}
165199
},
200+
166201
onError: () => setAlert("방 참여에 실패하였습니다."),
167202
});
203+
168204
onCall.current = false;
169-
}, [roomInfo?._id, history, triggerTags]);
205+
}, [roomInfo?._id, history, joinWithCarrier, triggerTags]); // 의존성 유지
170206

171207
const [taxiFare, setTaxiFare] = useState<number>(0);
172208
const getTaxiFare = async () => {
@@ -231,7 +267,7 @@ const BodyRoomSelection = ({
231267
<div css={styleMultipleInfo}>
232268
<div css={{ minWidth: 0 }}>
233269
<InfoSection title="탑승자" alignDirection="left">
234-
<Users values={roomInfo.part} />
270+
<Users values={participantsWithCarrier} />
235271
</InfoSection>
236272
</div>
237273
<div css={{ minWidth: "fit-content" }}>
@@ -266,6 +302,18 @@ const BodyRoomSelection = ({
266302
</InfoSection>
267303
) : null}
268304
</div>
305+
306+
{!isAlreadyPart && !isRoomFull && !isDepart && isLogin && (
307+
<div
308+
css={{ marginTop: "-5px", marginBottom: "12px", padding: "0 10px" }}
309+
>
310+
<CarrierOptionRow
311+
value={joinWithCarrier}
312+
handler={setJoinWithCarrier}
313+
/>{" "}
314+
</div>
315+
)}
316+
269317
{isLogin || isRoomFull || isDepart ? (
270318
<Button
271319
type="purple"

‎packages/web/src/components/ModalPopup/ModalRoomSelection.tsx‎

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useEffect, useMemo, useRef, useState } from "react";
22

3+
import { useValueRecoilState } from "@/hooks/useFetchRecoilState";
4+
import { useAxios } from "@/hooks/useTaxiAPI";
5+
6+
// 추가: 토글 API 호출용
37
import Modal from "@/components/Modal";
48
import Navigation from "@/components/Navigation";
59

@@ -8,8 +12,13 @@ import BodyRoomSelection, {
812
} from "./Body/BodyRoomSelection";
913
import { BodyRoomShare } from "./ModalRoomShare";
1014

15+
import alertAtom from "@/atoms/alert";
16+
import { useSetRecoilState } from "recoil";
17+
1118
import theme from "@/tools/theme";
1219

20+
// 추가
21+
1322
type HeightFixWrapperProps = {
1423
children: React.ReactNode;
1524
onChangeHeight?: (height: number) => void;
@@ -44,24 +53,59 @@ const HeightFixWrapper = (
4453
);
4554
};
4655

47-
const ModalRoomSelection = (
48-
{
49-
isOpen,
50-
onChangeIsOpen,
51-
roomInfo: _roomInfo,
52-
triggerTags,
53-
}: ModalRoomSelectionProps
54-
) => {
56+
const ModalRoomSelection = ({
57+
isOpen,
58+
onChangeIsOpen,
59+
roomInfo: _roomInfo,
60+
triggerTags,
61+
}: ModalRoomSelectionProps) => {
5562
const [roomInfo, setRoomInfo] = useState(_roomInfo);
5663
const [bodyHeight, setBodyHeight] = useState(0);
64+
65+
const axios = useAxios(); // 추가
66+
const setAlert = useSetRecoilState(alertAtom); // 추가
67+
const loginInfo = useValueRecoilState("loginInfo"); // 로그인된 내 정보
68+
69+
useEffect(() => {
70+
if (_roomInfo) setRoomInfo(_roomInfo);
71+
}, [_roomInfo]);
72+
73+
const handleToggleCarrier = () => {
74+
if (!roomInfo) return;
75+
axios({
76+
url: "/rooms/carrier/toggle",
77+
method: "post",
78+
data: { roomId: roomInfo._id }, // 필요한 경우 roomId 전송
79+
onSuccess: () => {
80+
// 성공 시 로컬 state 즉시 업데이트 (낙관적 업데이트)
81+
setRoomInfo((prev: any) => {
82+
if (!prev) return null;
83+
return {
84+
...prev,
85+
part: prev.part.map((user: any) =>
86+
user._id === loginInfo?.oid
87+
? { ...user, hasCarrier: !user.hasCarrier } // 내 상태 반전
88+
: user
89+
),
90+
};
91+
});
92+
},
93+
onError: () => setAlert("캐리어 상태 변경에 실패했습니다."),
94+
});
95+
};
96+
5797
const pages = useMemo(
5898
() =>
5999
roomInfo && [
60100
{
61101
key: "info",
62102
name: "방 정보",
63103
body: (
64-
<BodyRoomSelection roomInfo={roomInfo} triggerTags={triggerTags} />
104+
<BodyRoomSelection
105+
roomInfo={roomInfo}
106+
onToggleCarrier={handleToggleCarrier}
107+
triggerTags={triggerTags}
108+
/>
65109
),
66110
},
67111
{
@@ -70,13 +114,9 @@ const ModalRoomSelection = (
70114
body: <BodyRoomShare roomInfo={roomInfo} height={bodyHeight} />,
71115
},
72116
],
73-
[roomInfo, bodyHeight]
117+
[roomInfo, bodyHeight, loginInfo] // 의존성 추가
74118
);
75119

76-
useEffect(() => {
77-
if (_roomInfo) setRoomInfo(_roomInfo);
78-
}, [_roomInfo]);
79-
80120
const styleTitle = {
81121
...theme.font18,
82122
padding: "10px 32px 12px 8px",
@@ -91,9 +131,15 @@ const ModalRoomSelection = (
91131
{roomInfo && (
92132
<>
93133
<div css={styleTitle}>{roomInfo.name}</div>
134+
94135
{pages && <Navigation pages={pages} />}
136+
137+
{/* HeightFixWrapper에도 동일한 props를 넘겨야 높이 계산이 정확함 */}
95138
<HeightFixWrapper onChangeHeight={setBodyHeight}>
96-
<BodyRoomSelection roomInfo={roomInfo} />
139+
<BodyRoomSelection
140+
roomInfo={roomInfo}
141+
onToggleCarrier={handleToggleCarrier}
142+
/>
97143
</HeightFixWrapper>
98144
</>
99145
)}

0 commit comments

Comments
 (0)