Skip to content

Commit 172b261

Browse files
authored
[feat] 챗봇 추천페이지 단계별 api 연동 1차 반영 (#98)
* [refactor] scroll 관련 훅 분리 * [feat] greeting api 연결 * [feat] 단계별 api 3까지 연동 * [feat] 단계별 api 4단계 * [feat] step 넘어갈 시 radio disabled 처리 * [feat] radio disabled 선택한것만 제외 * [feat] ChatList 컴포넌트 분리 및 로딩처리 완료 * [style] 챗봇 레이아웃 수정 * [fix] 포맷팅 오류 수정
1 parent 1495933 commit 172b261

File tree

13 files changed

+508
-107
lines changed

13 files changed

+508
-107
lines changed

src/app/mypage/my-bar/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard';
32
import { Metadata } from 'next';
43

src/app/recommend/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Bg from '@/shared/assets/images/recommend_bg.webp';
44
function Page() {
55
return (
66
<div
7-
className="relative bg-repeat bg-auto w-full flex"
7+
className="relative bg-repeat bg-auto w-full flex flex-col overflow-hidden"
88
style={{ backgroundImage: `url(${Bg.src})` }}
99
>
1010
<h1 className="sr-only">취향추천하기</h1>

src/domains/recipe/details/DetailItem.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import Label from '@/domains/shared/components/label/Label';
44
import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph';
55
import { labelTitle } from '../utills/labelTitle';
66

7-
8-
97
interface Props {
108
name: string;
119
nameKo: string;

src/domains/recommend/api/chat.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { getApi } from '@/app/api/config/appConfig';
2+
import { useAuthStore } from '@/domains/shared/store/auth';
3+
import { ChatHistoryItem, ChatMessage, stepPayload, TextPayload } from '../types/recommend';
4+
5+
// 첫 시작 api
6+
export const fetchGreeting = async (message: string): Promise<ChatMessage | null> => {
7+
try {
8+
const userId = useAuthStore.getState().user?.id;
9+
if (!userId) throw new Error('userId 없음.');
10+
11+
const res = await fetch(`${getApi}/chatbot/greeting/${userId}`, {
12+
method: 'POST',
13+
headers: { 'Content-Type': 'application/json' },
14+
body: JSON.stringify({ message }),
15+
credentials: 'include',
16+
});
17+
18+
if (!res.ok) {
19+
console.error('API 요청 실패:', res.status, res.statusText);
20+
return null;
21+
}
22+
23+
const data = await res.json();
24+
25+
return {
26+
id: String(data.data.id),
27+
userId,
28+
message: data.data.message,
29+
sender: data.data.sender ?? 'CHATBOT',
30+
type: data.data.type,
31+
stepData: data.data.stepData ?? null,
32+
createdAt: data.data.timestamp,
33+
};
34+
} catch (err) {
35+
console.error('Greeting API 호출 에러:', err);
36+
return null;
37+
}
38+
};
39+
40+
// 유저 메시지 전송 (일반 텍스트)
41+
export const fetchSendTextMessage = async (payload: TextPayload): Promise<ChatMessage | null> => {
42+
try {
43+
const res = await fetch(`${getApi}/chatbot/chat`, {
44+
method: 'POST',
45+
headers: { 'Content-Type': 'application/json' },
46+
credentials: 'include',
47+
body: JSON.stringify(payload),
48+
});
49+
50+
if (!res.ok) return null;
51+
const data = await res.json();
52+
53+
return {
54+
id: String(data.data.id),
55+
userId: data.data.userId ?? payload.userId,
56+
message: data.data.message,
57+
sender: data.data.sender ?? 'CHATBOT',
58+
type: data.data.type,
59+
stepData: data.data.stepData ?? null,
60+
createdAt: data.data.timestamp,
61+
};
62+
} catch (err) {
63+
console.error('Text 메시지 전송 실패:', err);
64+
return null;
65+
}
66+
};
67+
68+
// 단계별 옵션 메시지 전송
69+
export const fetchSendStepMessage = async (payload: stepPayload): Promise<ChatMessage | null> => {
70+
try {
71+
const res = await fetch(`${getApi}/chatbot/chat`, {
72+
method: 'POST',
73+
headers: { 'Content-Type': 'application/json' },
74+
credentials: 'include',
75+
body: JSON.stringify(payload),
76+
});
77+
78+
if (!res.ok) return null;
79+
const data = await res.json();
80+
81+
return {
82+
id: String(data.data.id),
83+
userId: data.data.userId,
84+
message: data.data.message,
85+
sender: data.data.sender ?? 'CHATBOT',
86+
type: data.data.type,
87+
stepData: data.data.stepData ?? null,
88+
createdAt: data.data.timestamp,
89+
};
90+
} catch (err) {
91+
console.error('Step 메시지 전송 실패:', err);
92+
return null;
93+
}
94+
};
95+
96+
export const fetchChatHistory = async (): Promise<ChatMessage[] | null> => {
97+
try {
98+
const userId = useAuthStore.getState().user?.id;
99+
if (!userId) throw new Error('userId 없음');
100+
101+
const res = await fetch(`${getApi}/chatbot/history/user/${userId}`, {
102+
method: 'GET',
103+
credentials: 'include',
104+
});
105+
106+
if (!res.ok) {
107+
console.error('History API 요청 실패:', res.status, res.statusText);
108+
return null;
109+
}
110+
111+
const data = await res.json();
112+
113+
return data.data.map((item: ChatHistoryItem) => ({
114+
id: String(item.id),
115+
userId: item.userId,
116+
message: item.message,
117+
sender: item.sender,
118+
stepData: item.stepData ?? null,
119+
createdAt: item.timestamp,
120+
}));
121+
} catch (err) {
122+
console.error('History API 호출 에러:', err);
123+
return null;
124+
}
125+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ChatListProps } from '../types/recommend';
2+
import BotMessage from './bot/BotMessage';
3+
import NewMessageAlert from './bot/NewMessageAlert';
4+
import TypingIndicator from './bot/TypingIndicator';
5+
import UserMessage from './user/UserMessage';
6+
7+
function ChatList({
8+
messages,
9+
userCurrentStep,
10+
onSelectedOption,
11+
getRecommendations,
12+
chatListRef,
13+
chatEndRef,
14+
showNewMessageAlert,
15+
handleCheckBottom,
16+
handleScrollToBottom,
17+
isBotTyping,
18+
}: ChatListProps) {
19+
return (
20+
<div
21+
ref={chatListRef}
22+
onScroll={handleCheckBottom}
23+
className="absolute top-0 left-0 right-0 bottom-20 w-full gap-5 px-3 pt-12 pb-5 flex flex-col items-center overflow-y-auto pr-2"
24+
>
25+
<div className="max-w-1024 w-full">
26+
{messages.map((msg, idx) => {
27+
const isLastMessage = idx === messages.length - 1;
28+
const showTyping = isLastMessage && msg.sender === 'CHATBOT' && isBotTyping;
29+
30+
if (msg.sender === 'USER') {
31+
return <UserMessage key={`${msg.id}-${msg.sender}`} message={msg.message} />;
32+
}
33+
34+
return (
35+
<BotMessage
36+
key={`${msg.id}-${msg.sender}`}
37+
messages={[
38+
{
39+
id: msg.id,
40+
message: msg.message,
41+
type: msg.type ?? 'TEXT',
42+
options: msg.type === 'RADIO_OPTIONS' ? (msg.stepData?.options ?? []) : [],
43+
recommendations: getRecommendations(msg.type, msg.stepData),
44+
},
45+
]}
46+
stepData={msg.stepData}
47+
currentStep={userCurrentStep}
48+
onSelectedOption={onSelectedOption}
49+
/>
50+
);
51+
})}
52+
53+
{isBotTyping && <TypingIndicator />}
54+
55+
<div ref={chatEndRef}></div>
56+
{showNewMessageAlert && <NewMessageAlert onClick={handleScrollToBottom} />}
57+
</div>
58+
</div>
59+
);
60+
}
61+
62+
export default ChatList;

0 commit comments

Comments
 (0)