Skip to content

Commit 04a2b18

Browse files
authored
feat(fe): implement session termination (#218)
* feat: implement session termination and update session state handling * feat: add session termination modal
1 parent 4090068 commit 04a2b18

File tree

5 files changed

+125
-2
lines changed

5 files changed

+125
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useModalContext } from '@/features/modal';
2+
3+
import { Button } from '@/components';
4+
5+
interface SessionTerminateModalProps {
6+
onCancel?: () => void;
7+
onConfirm?: () => void;
8+
}
9+
10+
function SessionTerminateModal({
11+
onCancel,
12+
onConfirm,
13+
}: SessionTerminateModalProps) {
14+
const { closeModal } = useModalContext();
15+
16+
return (
17+
<div className='inline-flex flex-col items-center justify-center gap-2.5 rounded-lg bg-gray-50 p-8 shadow'>
18+
<div className='flex h-[8dvh] min-w-[20dvw] flex-col justify-center gap-2'>
19+
<div className='w-full text-center font-bold'>
20+
<span>정말 세션을 종료하시겠습니까?</span>
21+
</div>
22+
<div className='mx-auto mt-4 inline-flex w-full items-start justify-center gap-2.5'>
23+
<Button
24+
className='w-full bg-gray-500'
25+
onClick={() => {
26+
onCancel?.();
27+
closeModal();
28+
}}
29+
>
30+
<span className='flex-grow text-sm font-medium text-white'>
31+
취소하기
32+
</span>
33+
</Button>
34+
<Button
35+
className='w-full bg-indigo-600 transition-colors duration-200'
36+
onClick={() => {
37+
onConfirm?.();
38+
closeModal();
39+
}}
40+
>
41+
<span className='flex-grow text-sm font-medium text-white'>
42+
종료하기
43+
</span>
44+
</Button>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
}
50+
51+
export default SessionTerminateModal;

apps/client/src/components/qna/QuestionList.tsx

+37-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
import { isAxiosError } from 'axios';
12
import { motion } from 'motion/react';
23
import { useRef, useState } from 'react';
34
import { GrValidate } from 'react-icons/gr';
45
import { IoClose, IoShareSocialOutline } from 'react-icons/io5';
56

67
import { useModal } from '@/features/modal';
7-
import { useSessionStore } from '@/features/session';
8+
import { postSessionTerminate, useSessionStore } from '@/features/session';
89
import { useToastStore } from '@/features/toast';
910

1011
import {
1112
Button,
1213
CreateQuestionModal,
1314
SessionParticipantsModal,
1415
} from '@/components';
16+
import SessionTerminateModal from '@/components/modal/SessionTerminateModal';
1517
import QuestionSection from '@/components/qna/QuestionSection';
1618
import SessionSettingsDropdown from '@/components/qna/SessionSettingsDropdown';
1719

@@ -22,6 +24,8 @@ function QuestionList() {
2224
questions,
2325
sessionId,
2426
sessionTitle,
27+
sessionToken,
28+
setExpired,
2529
setSelectedQuestionId,
2630
} = useSessionStore();
2731

@@ -35,6 +39,36 @@ function QuestionList() {
3539
openModal: openSessionParticipantsModal,
3640
} = useModal(<SessionParticipantsModal />);
3741

42+
const { Modal: SessionTerminate, openModal: openSessionTerminateModal } =
43+
useModal(
44+
<SessionTerminateModal
45+
onConfirm={() => {
46+
if (!sessionId || !sessionToken) return;
47+
48+
postSessionTerminate({ sessionId, token: sessionToken })
49+
.then((response) => {
50+
if (response.expired) {
51+
setExpired(true);
52+
addToast({
53+
type: 'SUCCESS',
54+
message: '세션이 종료되었습니다',
55+
duration: 3000,
56+
});
57+
}
58+
})
59+
.catch((err) => {
60+
if (isAxiosError(err) && err.response?.status === 403) {
61+
addToast({
62+
type: 'ERROR',
63+
message: '세션 생성자만 세션을 종료할 수 있습니다',
64+
duration: 3000,
65+
});
66+
}
67+
});
68+
}}
69+
/>,
70+
);
71+
3872
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
3973

4074
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -98,7 +132,7 @@ function QuestionList() {
98132
{
99133
icon: <IoClose />,
100134
label: '세션 종료',
101-
onClick: () => {},
135+
onClick: () => openSessionTerminateModal(),
102136
},
103137
];
104138

@@ -150,6 +184,7 @@ function QuestionList() {
150184
</div>
151185
{CreateQuestion}
152186
{SessionParticipants}
187+
{SessionTerminate}
153188
</>
154189
);
155190
}

apps/client/src/features/session/session.api.ts

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
PostSessionRequestSchema,
1818
PostSessionResponseDTO,
1919
PostSessionResponseSchema,
20+
PostSessionTerminateRequestDTO,
21+
PostSessionTerminateRequestSchema,
22+
PostSessionTerminateResponseSchema,
2023
} from '@/features/session/session.dto';
2124

2225
export const postSession = (body: PostSessionRequestDTO) =>
@@ -73,3 +76,14 @@ export const patchSessionHost = (
7376
PatchSessionHostRequestSchema.parse(body),
7477
)
7578
.then((res) => PatchSessionHostResponseSchema.parse(res.data));
79+
80+
export const postSessionTerminate = ({
81+
token,
82+
sessionId,
83+
}: PostSessionTerminateRequestDTO & { sessionId: string }) =>
84+
axios
85+
.post(
86+
`/api/sessions/${sessionId}/terminate`,
87+
PostSessionTerminateRequestSchema.parse({ token }),
88+
)
89+
.then((res) => PostSessionTerminateResponseSchema.parse(res.data));

apps/client/src/features/session/session.dto.ts

+14
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export const PatchSessionHostResponseSchema = z.object({
4848
user: UserSchema,
4949
});
5050

51+
export const PostSessionTerminateRequestSchema = z.object({
52+
token: z.string(),
53+
});
54+
55+
export const PostSessionTerminateResponseSchema = z.object({
56+
expired: z.boolean(),
57+
});
58+
5159
export type PostSessionRequestDTO = z.infer<typeof PostSessionRequestSchema>;
5260
export type PostSessionResponseDTO = z.infer<typeof PostSessionResponseSchema>;
5361
export type GetSessionsResponseDTO = z.infer<typeof GetSessionsResponseSchema>;
@@ -66,3 +74,9 @@ export type PatchSessionHostRequestDTO = z.infer<
6674
export type PatchSessionHostResponseDTO = z.infer<
6775
typeof PatchSessionHostResponseSchema
6876
>;
77+
export type PostSessionTerminateRequestDTO = z.infer<
78+
typeof PostSessionTerminateRequestSchema
79+
>;
80+
export type PostSessionTerminateResponseDTO = z.infer<
81+
typeof PostSessionTerminateResponseSchema
82+
>;

apps/client/src/features/socket/socket.service.ts

+9
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ export class SocketService {
132132
});
133133
},
134134
);
135+
136+
this.socket.on('sessionEnded', () => {
137+
store.setExpired(true);
138+
useToastStore.getState().addToast({
139+
type: 'INFO',
140+
message: '세션이 종료되었습니다.',
141+
duration: 3000,
142+
});
143+
});
135144
}
136145

137146
sendChatMessage(message: string) {

0 commit comments

Comments
 (0)