Skip to content

Commit cc75092

Browse files
committed
refactor: chatbot 메시지 로직 커스텀 훅으로 분리
1 parent d9fc6a7 commit cc75092

2 files changed

Lines changed: 93 additions & 75 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useChatHistoryQuery, useSendMessageMutation } from '@shared/apis/chatbot';
2+
import { AxiosError } from 'axios';
3+
import { ERROR_MESSAGES, INITIAL_BOT_MESSAGE } from './constants';
4+
import type { ChatMessage } from './types';
5+
6+
const getErrorMessage = (error: unknown): string => {
7+
if (error instanceof AxiosError) {
8+
const serverMessage = error.response?.data?.message;
9+
if (serverMessage) {
10+
return serverMessage;
11+
}
12+
13+
const status = error.response?.status;
14+
if (status === 429) {
15+
return ERROR_MESSAGES.RATE_LIMIT;
16+
}
17+
if (status === 502) {
18+
return ERROR_MESSAGES.API_CONNECTION;
19+
}
20+
}
21+
22+
return ERROR_MESSAGES.DEFAULT;
23+
};
24+
25+
export const useChatMessages = () => {
26+
const { data: history, isLoading: isHistoryLoading } = useChatHistoryQuery();
27+
const sendMutation = useSendMessageMutation();
28+
29+
const messages: ChatMessage[] = (() => {
30+
if (!history || history.length === 0) {
31+
return [
32+
{
33+
id: 'bot-initial',
34+
role: 'bot' as const,
35+
content: INITIAL_BOT_MESSAGE,
36+
status: 'done',
37+
},
38+
];
39+
}
40+
41+
return history.map((item, index) => ({
42+
id: `history-${index}`,
43+
role: item.role === 'User' ? ('user' as const) : ('bot' as const),
44+
content: item.message,
45+
status: 'done' as const,
46+
}));
47+
})();
48+
49+
const displayMessages: ChatMessage[] = (() => {
50+
if (!sendMutation.isPending) {
51+
return messages;
52+
}
53+
54+
const pendingUserMessage: ChatMessage = {
55+
id: 'pending-user',
56+
role: 'user',
57+
content: sendMutation.variables ?? '',
58+
status: 'done',
59+
};
60+
61+
const loadingBotMessage: ChatMessage = {
62+
id: 'pending-bot',
63+
role: 'bot',
64+
content: '',
65+
status: 'loading',
66+
};
67+
68+
return [...messages, pendingUserMessage, loadingBotMessage];
69+
})();
70+
71+
const handleSend = (value: string) => {
72+
const trimmed = value.trim();
73+
if (!trimmed || sendMutation.isPending) {
74+
return;
75+
}
76+
77+
sendMutation.mutate(trimmed);
78+
};
79+
80+
const errorMessage = sendMutation.isError ? getErrorMessage(sendMutation.error) : null;
81+
82+
return {
83+
displayMessages,
84+
isHistoryLoading,
85+
errorMessage,
86+
handleSend,
87+
};
88+
};

src/pages/chatbot/ui/ChatbotPage.tsx

Lines changed: 5 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,17 @@
1-
import { useChatHistoryQuery, useSendMessageMutation } from '@shared/apis/chatbot';
21
import { Logo4 } from '@shared/assets/logo';
32
import { ChatInput } from '@shared/ui/ChatInput';
43
import { LoadingFallback } from '@shared/ui/LoadingFallback';
5-
import { useEffect, useMemo, useRef } from 'react';
4+
import { useEffect, useRef } from 'react';
65
import { ChatMessageList } from './ChatMessageList';
7-
import type { ChatMessage } from '../model/types';
8-
9-
const INITIAL_BOT_MESSAGE = `루핏이 예상 수리비를 빠르게 계산해드릴게요. 아래 3가지만 알려주세요.
10-
(견적은 추정치이며 실제 비용은 수리점/부품/상태에 따라 달라질 수 있어요.)
11-
12-
기종: 예) 아이폰 15, 갤럭시 S23
13-
14-
어디가 고장났는지: 예) 액정/배터리/카메라/후면/충전/침수
15-
16-
보험/케어 가입 여부: 예) 애플케어 O/X, 통신사 보험 O/X
17-
18-
가능하면 원하는 방향도 한 줄로 적어주세요: "정품 우선" / "최대한 저렴하게" / "빨리"
19-
20-
예시) "아이폰 15, 액정 깨짐, 애플케어 X, 최대한 저렴하게"
21-
22-
`;
6+
import { useChatMessages } from '../model/useChatMessages';
237

248
const ChatbotPage = () => {
25-
const { data: history, isLoading: isHistoryLoading } = useChatHistoryQuery();
26-
const sendMutation = useSendMessageMutation();
9+
const { displayMessages, isHistoryLoading, errorMessage, handleSend } = useChatMessages();
2710
const endOfMessagesRef = useRef<HTMLDivElement | null>(null);
2811

29-
const messages: ChatMessage[] = useMemo(() => {
30-
if (!history || history.length === 0) {
31-
return [
32-
{
33-
id: 'bot-initial',
34-
role: 'bot' as const,
35-
content: INITIAL_BOT_MESSAGE,
36-
status: 'done',
37-
},
38-
];
39-
}
40-
41-
return history.map((item, index) => ({
42-
id: `history-${index}`,
43-
role: item.role === 'User' ? ('user' as const) : ('bot' as const),
44-
content: item.message,
45-
status: 'done' as const,
46-
}));
47-
}, [history]);
48-
49-
const displayMessages: ChatMessage[] = useMemo(() => {
50-
if (!sendMutation.isPending) {
51-
return messages;
52-
}
53-
54-
const pendingUserMessage: ChatMessage = {
55-
id: 'pending-user',
56-
role: 'user',
57-
content: sendMutation.variables ?? '',
58-
status: 'done',
59-
};
60-
61-
const loadingBotMessage: ChatMessage = {
62-
id: 'pending-bot',
63-
role: 'bot',
64-
content: '',
65-
status: 'loading',
66-
};
67-
68-
return [...messages, pendingUserMessage, loadingBotMessage];
69-
}, [messages, sendMutation.isPending, sendMutation.variables]);
70-
7112
useEffect(() => {
7213
endOfMessagesRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
73-
}, [displayMessages]);
74-
75-
const handleSend = (value: string) => {
76-
const trimmed = value.trim();
77-
if (!trimmed || sendMutation.isPending) {
78-
return;
79-
}
80-
81-
sendMutation.mutate(trimmed);
82-
};
14+
}, [displayMessages.length]);
8315

8416
if (isHistoryLoading) {
8517
return <LoadingFallback message="대화를 불러오는 중" />;
@@ -97,9 +29,7 @@ const ChatbotPage = () => {
9729
<h1 className="typo-title-2 text-black">루핏봇</h1>
9830
</div>
9931
<ChatMessageList messages={displayMessages} />
100-
{sendMutation.isError && (
101-
<div className="w-full text-center text-red-500">메시지 전송에 실패했습니다. 다시 시도해주세요.</div>
102-
)}
32+
{errorMessage && <div className="w-full text-center text-red-500">{errorMessage}</div>}
10333
<div ref={endOfMessagesRef} className="scroll-mb-[96px]" />
10434
</section>
10535
<div className="md:px-xxxl fixed inset-x-0 bottom-0 z-50 bg-white px-(--margin-l) pt-4 pb-(--margin-s) xl:px-[120px]">

0 commit comments

Comments
 (0)