diff --git a/src/features/Common/AIChatModal.tsx b/src/features/Common/AIChatModal.tsx index ef4c5b9..457e7e8 100644 --- a/src/features/Common/AIChatModal.tsx +++ b/src/features/Common/AIChatModal.tsx @@ -1,4 +1,4 @@ -import { useQueryClient } from '@tanstack/react-query' // 1. useQueryClient 임포트 +import { useQuery, useQueryClient } from '@tanstack/react-query' import React, { useEffect, useRef, useState } from 'react' import ChatIcon from '@/assets/icons/common/chat.svg' @@ -20,11 +20,31 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { const [isLoading, setIsLoading] = useState(false) const chatBoxRef = useRef(null) + const { data: historyData, isLoading: isHistoryLoading } = useQuery({ + queryKey: ['chatHistory'], + queryFn: nlpApi.getHistory, + }) + + useEffect(() => { + if (historyData?.isSuccess && historyData.result) { + const rawMessages = historyData.result.messages + const mappedMessages: ChatMessage[] = Array.isArray(rawMessages) + ? rawMessages.map((msg) => ({ + id: crypto.randomUUID(), + sender: msg.role === 'user' ? 'user' : 'bot', + text: msg.content ?? '', + })) + : [] + + setMessages(mappedMessages) + } + }, [historyData]) + useEffect(() => { if (chatBoxRef.current) { chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight } - }, [messages, isLoading]) + }, [messages, isLoading, isHistoryLoading]) const handleSendMessage = async () => { if (!inputValue.trim() || isLoading) return @@ -52,6 +72,8 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { } setMessages((prev) => [...prev, botMessage]) + queryClient.invalidateQueries({ queryKey: ['chatHistory'] }) + if (response.result.action === 'UPDATED') { queryClient.invalidateQueries({ queryKey: ['calendar'] }) queryClient.invalidateQueries({ queryKey: ['events'] }) @@ -67,14 +89,18 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { }, ]) } - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'AI 비서 서버와 연결에 실패했습니다. 잠시 후 다시 시도해주세요.' + setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), sender: 'bot', - text: 'AI 비서 서버와 연결에 실패했습니다. 잠시 후 다시 시도해주세요.', + text: `연결에 실패했습니다. (원인: ${errorMessage})`, }, ]) } finally { @@ -88,7 +114,7 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { } } - const isChatEmpty = messages.length === 0 && !isLoading + const isChatEmpty = messages.length === 0 && !isLoading && !isHistoryLoading const hideEmptyChatBox = !isHome && isChatEmpty return ( @@ -101,7 +127,14 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { - {isChatEmpty ? ( + {isHistoryLoading ? ( + + robot + + 잠시만 기다려주세요... + + + ) : isChatEmpty ? ( 채팅 시작 @@ -150,11 +183,11 @@ function AIChatModal({ isHome = true }: AIChatModalProps) { onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="예시) 내일 오후 3시 치과 진료 받으러 감" - disabled={isLoading} + disabled={isLoading || isHistoryLoading} /> ↑ diff --git a/src/features/Friends/SharedScheduleItem.tsx b/src/features/Friends/SharedScheduleItem.tsx index 1d19a6c..53cad40 100644 --- a/src/features/Friends/SharedScheduleItem.tsx +++ b/src/features/Friends/SharedScheduleItem.tsx @@ -1,3 +1,9 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' + +import { eventShareApi } from '@/shared/api/friends/eventShare' +import { getErrorMessage } from '@/shared/utils' +import { useToastStore } from '@/store/useToastStore' + import * as S from './SharedScheduleItem.style' interface SharedScheduleItemProps { @@ -19,6 +25,9 @@ export default function SharedScheduleItem({ accentColor = '#5c6ac4', onCancelSuccess, }: SharedScheduleItemProps) { + const queryClient = useQueryClient() + const { showToast } = useToastStore() + const formatDate = (dateStr: string) => { if (!dateStr) return '' const date = new Date(dateStr) @@ -35,9 +44,45 @@ export default function SharedScheduleItem({ ? formatDate(startDate) : `${formatDate(startDate)} - ${formatDate(endDate)}` + const leaveEventMutation = useMutation({ + mutationFn: () => eventShareApi.leaveEvent(eventId), + onSuccess: (response) => { + if (response.isSuccess) { + queryClient.invalidateQueries({ queryKey: ['calendar'] }) + queryClient.invalidateQueries({ queryKey: ['events'] }) + queryClient.invalidateQueries({ queryKey: ['todos'] }) + + showToast({ + title: '그룹 탈퇴 완료', + message: '성공적으로 탈퇴되었습니다.', + toastType: 'success', + }) + + onCancelSuccess?.() + } else { + showToast({ + title: '탈퇴 처리 실패', + message: response.message || '탈퇴 처리에 실패했습니다.', + toastType: 'error', + }) + } + }, + onError: (error) => { + console.error(error) + const errorMessage = getErrorMessage(error) + + showToast({ + title: '오류 발생', + message: errorMessage || '서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요.', + toastType: 'error', + }) + }, + }) + const handleCancelShare = () => { - console.log(`eventId ${eventId} 공유 취소 클릭`) - onCancelSuccess?.() + if (window.confirm('정말 이 공유 이벤트에서 탈퇴하시겠습니까?')) { + leaveEventMutation.mutate() + } } return ( @@ -50,8 +95,13 @@ export default function SharedScheduleItem({ 공유자: {sharerName} - - 공유 취소 + + {leaveEventMutation.isPending ? '취소 중...' : '공유 취소'} diff --git a/src/shared/api/friends/eventShare.ts b/src/shared/api/friends/eventShare.ts index ceb5742..d234902 100644 --- a/src/shared/api/friends/eventShare.ts +++ b/src/shared/api/friends/eventShare.ts @@ -1,6 +1,7 @@ import type { ActionApiResponse, InvitationsApiResponse, + LeaveEventResponse, SharedEventsApiResponse, } from '@/shared/types/eventShare/eventShare' @@ -32,4 +33,10 @@ export const eventShareApi = { const { data } = await axiosInstance.get(`${BASE_URL}/invitations`) return data }, + leaveEvent: async (eventId: number): Promise => { + const { data } = await axiosInstance.delete( + `/events/${eventId}/participants/leave`, + ) + return data + }, } diff --git a/src/shared/api/home/home.ts b/src/shared/api/home/home.ts index 7bb53ca..7458adc 100644 --- a/src/shared/api/home/home.ts +++ b/src/shared/api/home/home.ts @@ -1,5 +1,6 @@ import type { BriefingResponse, + ChatHistoryResponse, ChatResponse, ReminderResponse, SuggestionListResponse, @@ -47,4 +48,8 @@ export const nlpApi = { const res = await axiosInstance.post('/chat', { message }) return res.data }, + getHistory: async (): Promise => { + const res = await axiosInstance.get('/chat/history') + return res.data + }, } diff --git a/src/shared/types/eventShare/eventShare.ts b/src/shared/types/eventShare/eventShare.ts index 3999690..43ffe2e 100644 --- a/src/shared/types/eventShare/eventShare.ts +++ b/src/shared/types/eventShare/eventShare.ts @@ -32,3 +32,5 @@ export interface InvitationsResult { export type InvitationsApiResponse = TCommonResponse export type ActionApiResponse = TCommonResponse + +export type LeaveEventResponse = TCommonResponse diff --git a/src/shared/types/home/home.ts b/src/shared/types/home/home.ts index 732e8dc..326252d 100644 --- a/src/shared/types/home/home.ts +++ b/src/shared/types/home/home.ts @@ -75,3 +75,16 @@ export interface ChatMessage { text: string action?: ChatActionType } + +export type ChatHistoryResponse = TCommonResponse + +export interface ChatHistory { + messages: boolean | undefined + result: { + messages: { + role: 'user' | 'assistant' + content: string + }[] + summary: string | null + } +}