Skip to content
Merged
1 change: 0 additions & 1 deletion src/app/mypage/my-bar/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard';
import { Metadata } from 'next';

Expand Down
2 changes: 1 addition & 1 deletion src/app/recommend/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Bg from '@/shared/assets/images/recommend_bg.webp';
function Page() {
return (
<div
className="relative bg-repeat bg-auto w-full flex"
className="relative bg-repeat bg-auto w-full flex flex-col overflow-hidden"
style={{ backgroundImage: `url(${Bg.src})` }}
>
<h1 className="sr-only">취향추천하기</h1>
Expand Down
2 changes: 0 additions & 2 deletions src/domains/recipe/details/DetailItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import Label from '@/domains/shared/components/label/Label';
import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph';
import { labelTitle } from '../utills/labelTitle';



interface Props {
name: string;
nameKo: string;
Expand Down
125 changes: 125 additions & 0 deletions src/domains/recommend/api/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getApi } from '@/app/api/config/appConfig';
import { useAuthStore } from '@/domains/shared/store/auth';
import { ChatHistoryItem, ChatMessage, stepPayload, TextPayload } from '../types/recommend';

// 첫 시작 api
export const fetchGreeting = async (message: string): Promise<ChatMessage | null> => {
try {
const userId = useAuthStore.getState().user?.id;
if (!userId) throw new Error('userId 없음.');

const res = await fetch(`${getApi}/chatbot/greeting/${userId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
credentials: 'include',
});

if (!res.ok) {
console.error('API 요청 실패:', res.status, res.statusText);
return null;
}

const data = await res.json();

return {
id: String(data.data.id),
userId,
message: data.data.message,
sender: data.data.sender ?? 'CHATBOT',
type: data.data.type,
stepData: data.data.stepData ?? null,
createdAt: data.data.timestamp,
};
} catch (err) {
console.error('Greeting API 호출 에러:', err);
return null;
}
};

// 유저 메시지 전송 (일반 텍스트)
export const fetchSendTextMessage = async (payload: TextPayload): Promise<ChatMessage | null> => {
try {
const res = await fetch(`${getApi}/chatbot/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
});

if (!res.ok) return null;
const data = await res.json();

return {
id: String(data.data.id),
userId: data.data.userId ?? payload.userId,
message: data.data.message,
sender: data.data.sender ?? 'CHATBOT',
type: data.data.type,
stepData: data.data.stepData ?? null,
createdAt: data.data.timestamp,
};
} catch (err) {
console.error('Text 메시지 전송 실패:', err);
return null;
}
};

// 단계별 옵션 메시지 전송
export const fetchSendStepMessage = async (payload: stepPayload): Promise<ChatMessage | null> => {
try {
const res = await fetch(`${getApi}/chatbot/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
});

if (!res.ok) return null;
const data = await res.json();

return {
id: String(data.data.id),
userId: data.data.userId,
message: data.data.message,
sender: data.data.sender ?? 'CHATBOT',
type: data.data.type,
stepData: data.data.stepData ?? null,
createdAt: data.data.timestamp,
};
} catch (err) {
console.error('Step 메시지 전송 실패:', err);
return null;
}
};

export const fetchChatHistory = async (): Promise<ChatMessage[] | null> => {
try {
const userId = useAuthStore.getState().user?.id;
if (!userId) throw new Error('userId 없음');

const res = await fetch(`${getApi}/chatbot/history/user/${userId}`, {
method: 'GET',
credentials: 'include',
});

if (!res.ok) {
console.error('History API 요청 실패:', res.status, res.statusText);
return null;
}

const data = await res.json();

return data.data.map((item: ChatHistoryItem) => ({
id: String(item.id),
userId: item.userId,
message: item.message,
sender: item.sender,
stepData: item.stepData ?? null,
createdAt: item.timestamp,
}));
} catch (err) {
console.error('History API 호출 에러:', err);
return null;
}
};
62 changes: 62 additions & 0 deletions src/domains/recommend/components/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ChatListProps } from '../types/recommend';
import BotMessage from './bot/BotMessage';
import NewMessageAlert from './bot/NewMessageAlert';
import TypingIndicator from './bot/TypingIndicator';
import UserMessage from './user/UserMessage';

function ChatList({
messages,
userCurrentStep,
onSelectedOption,
getRecommendations,
chatListRef,
chatEndRef,
showNewMessageAlert,
handleCheckBottom,
handleScrollToBottom,
isBotTyping,
}: ChatListProps) {
return (
<div
ref={chatListRef}
onScroll={handleCheckBottom}
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"
>
<div className="max-w-1024 w-full">
{messages.map((msg, idx) => {
const isLastMessage = idx === messages.length - 1;
const showTyping = isLastMessage && msg.sender === 'CHATBOT' && isBotTyping;

if (msg.sender === 'USER') {
return <UserMessage key={`${msg.id}-${msg.sender}`} message={msg.message} />;
}

return (
<BotMessage
key={`${msg.id}-${msg.sender}`}
messages={[
{
id: msg.id,
message: msg.message,
type: msg.type ?? 'TEXT',
options: msg.type === 'RADIO_OPTIONS' ? (msg.stepData?.options ?? []) : [],
recommendations: getRecommendations(msg.type, msg.stepData),
},
]}
stepData={msg.stepData}
currentStep={userCurrentStep}
onSelectedOption={onSelectedOption}
/>
);
})}

{isBotTyping && <TypingIndicator />}

<div ref={chatEndRef}></div>
{showNewMessageAlert && <NewMessageAlert onClick={handleScrollToBottom} />}
</div>
</div>
);
}

export default ChatList;
Loading