Skip to content

Commit a47fb14

Browse files
authored
[fix] 챗봇 추천 페이지 수정 및 기능추가 (#130)
* [fix] 논알콜일 시 payload 값 수정 * [fix] options 기본으로 수정 * [feat] 다시 시작하기 추가 * [feat] 채팅 캡처기능 추가 * [fix] currentStep 제거 * [fix] 파비콘 추가 및 헤더 메뉴 네이밍 수정 * [style] layout 모바일 대응 추가
1 parent 91b3b8e commit a47fb14

File tree

18 files changed

+181
-65
lines changed

18 files changed

+181
-65
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
"@tanstack/react-virtual": "^3.13.12",
2323
"class-variance-authority": "^0.7.1",
2424
"gsap": "^3.13.0",
25+
"html-to-image": "^1.11.13",
2526
"lottie-react": "^2.4.1",
2627
"next": "15.5.3",
2728
"react": "19.1.0",
2829
"react-dom": "19.1.0",
2930
"react-hot-toast": "^2.6.0",
31+
"react-intersection-observer": "^9.16.0",
3032
"react-use": "^17.6.0",
31-
"swiper": "^12.0.2",
32-
"react-intersection-observer": "^9.16.0"
33+
"swiper": "^12.0.2"
3334
},
3435
"devDependencies": {
3536
"@eslint/eslintrc": "^3",

public/favicon.ico

15 KB
Binary file not shown.

src/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const metadata: Metadata = {
1515
title: { default: 'SSOUL', template: 'SSOUL | %s' },
1616
metadataBase: new URL('http://www.ssoul.life'),
1717
description: '칵테일을 좋아하는 사람들을 위한 서비스',
18+
icons: '/favicon.ico',
1819
};
1920

2021
export default function RootLayout({
@@ -24,7 +25,7 @@ export default function RootLayout({
2425
}>) {
2526
return (
2627
<html lang="ko-KR">
27-
<body className="relative flex flex-col min-h-screen">
28+
<body className="relative flex flex-col min-h-full-screen">
2829
<Provider>
2930
<Header />
3031
<ClientInitHook />

src/domains/recommend/components/ChatList.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@ import BotMessage from './bot/BotMessage';
44
import NewMessageAlert from './bot/NewMessageAlert';
55
import UserMessage from './user/UserMessage';
66

7-
function ChatList({ messages, userCurrentStep, onSelectedOption }: ChatListProps) {
7+
function ChatList({ messages, userCurrentStep, onSelectedOption, chatRef }: ChatListProps) {
88
const { chatListRef, chatEndRef, showNewMessageAlert, handleCheckBottom, handleScrollToBottom } =
99
useChatScroll(messages[messages.length - 1]?.id);
1010

11-
const getRecommendations = (
12-
type: string | undefined,
13-
stepData?: StepRecommendation | null
14-
): RecommendationItem[] => {
15-
if (type !== 'CARD_LIST' || !stepData?.recommendations) return [];
16-
return stepData.recommendations;
11+
const combinedRef = (el: HTMLDivElement) => {
12+
chatListRef.current = el;
13+
if (chatRef) chatRef.current = el;
1714
};
1815

1916
return (
2017
<div
21-
ref={chatListRef}
18+
ref={combinedRef}
2219
onScroll={handleCheckBottom}
2320
className="absolute top-8 left-0 bottom-18 sm:bottom-21 w-full gap-5 px-3 pt-7 pb-4 flex flex-col items-center overflow-y-auto pr-2"
2421
>
@@ -35,8 +32,6 @@ function ChatList({ messages, userCurrentStep, onSelectedOption }: ChatListProps
3532

3633
const isTyping = msg.type === 'TYPING';
3734

38-
const recommendations = getRecommendations(msg.type, msg.stepData);
39-
4035
return (
4136
<BotMessage
4237
key={keyId}
@@ -45,12 +40,10 @@ function ChatList({ messages, userCurrentStep, onSelectedOption }: ChatListProps
4540
id: msg.id,
4641
message: msg.message,
4742
type: msg.type ?? 'TEXT',
48-
options: msg.type === 'RADIO_OPTIONS' ? (msg.stepData?.options ?? []) : [],
49-
recommendations,
43+
stepData: msg.stepData,
5044
},
5145
]}
5246
showProfile={showProfile}
53-
stepData={msg.stepData}
5447
currentStep={userCurrentStep}
5548
onSelectedOption={onSelectedOption}
5649
isTyping={isTyping}

src/domains/recommend/components/ChatPreview.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Image from 'next/image';
33
import Send from '@/shared/assets/icons/send_36.svg';
44
import Link from 'next/link';
55
import { setPreLoginPath } from '@/domains/shared/auth/utils/setPreLoginPath';
6+
import Crop from '@/shared/assets/icons/crop_32.svg';
67

78
function ChatPreview() {
89
return (
@@ -27,7 +28,7 @@ function ChatPreview() {
2728
<div>
2829
<div className="flex flex-col w-fit max-w-[80%] min-w-[120px] p-3 rounded-2xl rounded-tl-none bg-white text-black opacity-0 animate-fadeIn">
2930
<div>
30-
<p className="whitespace-pre-line">취향에 맞는 칵테일, 저와 함께 찾아볼까요?</p>
31+
<p className="whitespace-pre-line">취향에 맞는 칵테일🤩 저와 함께 찾아볼까요?</p>
3132
<Link
3233
href="/login"
3334
onNavigate={async () => {
@@ -45,7 +46,14 @@ function ChatPreview() {
4546

4647
<div className="fixed left-0 bottom-0 w-full px-3 py-4 flex-center bg-primary">
4748
<form onSubmit={(e) => e.preventDefault()} className="w-full max-w-[64rem]">
48-
<div className="flex items-end gap-2">
49+
<div className="flex items-center gap-2">
50+
<button
51+
aria-label="채팅 이미지 저장"
52+
title="채팅내용 이미지 저장"
53+
className="flex-center rounded-full sm:bg-secondary/20 sm:w-10 sm:h-10 hover:bg-white/10 active:bg-white/10"
54+
>
55+
<Crop />
56+
</button>
4957
<label htmlFor="chatInput" className="sr-only">
5058
질문 입력창
5159
</label>

src/domains/recommend/components/ChatSection.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useRef, useState } from 'react';
44
import MessageInput from './user/MessageInput';
5-
import { fetchSendStepMessage, fetchSendTextMessage } from '../api/chat';
5+
import { fetchGreeting, fetchSendStepMessage, fetchSendTextMessage } from '../api/chat';
66
import { ChatMessage, stepPayload } from '../types/recommend';
77
import ChatList from './ChatList';
88
import { useSelectedOptions } from '../hook/useSelectedOptions';
99
import { useAuthStore } from '@/domains/shared/store/auth';
1010
import { useChatInit } from '../hook/useChatInit';
1111
import { useChatWarning } from '../hook/useChatWarning';
12+
import { useChatCapture } from '../hook/useChatCapture';
1213

1314
function ChatSection() {
1415
const [messages, setMessages] = useState<ChatMessage[]>([]);
1516
const [userCurrentStep, setUserCurrentStep] = useState(0);
1617
const { selectedOptions, setOption, setStepOption } = useSelectedOptions();
18+
const chatRef = useRef<HTMLDivElement>(null);
19+
const { capture } = useChatCapture(chatRef);
1720

1821
const isInputDisabled =
1922
selectedOptions.current.selectedSearchType !== 'QA' && userCurrentStep < 3;
@@ -77,6 +80,25 @@ function ChatSection() {
7780
const userId = useAuthStore.getState().user?.id;
7881
if (!userId) return;
7982

83+
// RESTART 처리
84+
if (value === 'RESTART') {
85+
setUserCurrentStep(0);
86+
setMessages([]);
87+
88+
// 초기 인사 불러오기
89+
try {
90+
const greeting = await fetchGreeting('');
91+
if (greeting) setMessages([greeting]);
92+
} catch (err) {
93+
console.error('인사 메시지 불러오기 실패:', err);
94+
}
95+
96+
// 선택된 옵션 초기화
97+
setOption('selectedSearchType', '');
98+
setStepOption(0, '');
99+
return;
100+
}
101+
80102
const tempId = Date.now().toString();
81103
const tempCreatedAt = new Date().toISOString();
82104

@@ -131,11 +153,12 @@ function ChatSection() {
131153
⚠️ 페이지를 벗어나면 채팅내용이 사라집니다.
132154
</div>
133155
<ChatList
156+
chatRef={chatRef}
134157
messages={messages}
135158
userCurrentStep={userCurrentStep}
136159
onSelectedOption={handleSelectedOption}
137160
/>
138-
<MessageInput onSubmit={handleSubmitText} disabled={isInputDisabled} />
161+
<MessageInput onSubmit={handleSubmitText} onCapture={capture} disabled={isInputDisabled} />
139162
</section>
140163
);
141164
}

src/domains/recommend/components/bot/BotCocktailCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function BotCocktailCard({ cocktailId, cocktailNameKo, cocktailImgUrl }: Recomme
1919
className="object-cover"
2020
alt={cocktailNameKo}
2121
sizes="200px"
22+
crossOrigin="anonymous"
2223
priority
2324
/>
2425
</div>

src/domains/recommend/components/bot/BotMessage.tsx

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ interface BotMessage {
1212
id: string;
1313
message: string;
1414
type: string;
15-
options?: StepOption[];
16-
recommendations?: RecommendationItem[];
15+
stepData?: StepRecommendation | null;
1716
}
1817

1918
interface BotMessages {
2019
messages: BotMessage[];
2120
showProfile: boolean;
22-
stepData?: StepRecommendation | null;
2321
currentStep?: number;
2422
onSelectedOption?: (value: string) => void;
2523
isTyping?: boolean;
@@ -28,12 +26,16 @@ interface BotMessages {
2826
function BotMessage({
2927
messages,
3028
showProfile,
31-
stepData,
3229
currentStep,
3330
onSelectedOption,
3431
isTyping,
3532
}: BotMessages) {
36-
const [selected, setSelected] = useState('');
33+
const [selectedOptions, setSelectedOptions] = useState<Record<number, string>>({});
34+
35+
const handleOptionChange = (step: number, value: string) => {
36+
setSelectedOptions((prev) => ({ ...prev, [step]: value }));
37+
onSelectedOption?.(value);
38+
};
3739

3840
return (
3941
<article aria-label="취향추천 챗봇 메시지" className="">
@@ -66,34 +68,47 @@ function BotMessage({
6668
</div>
6769

6870
{/* radio */}
69-
{msg.type === 'RADIO_OPTIONS' && msg.options?.length && (
71+
{msg.type === 'RADIO_OPTIONS' && msg.stepData?.options?.length ? (
7072
<BotOptions
71-
options={msg.options}
72-
step={stepData?.currentStep ?? 0}
73-
currentStep={currentStep ?? 0}
74-
value={selected}
75-
onChange={(val) => {
76-
setSelected(val);
77-
onSelectedOption?.(val);
78-
}}
73+
options={msg.stepData.options}
74+
step={msg.stepData.currentStep ?? 0}
75+
value={selectedOptions[msg.stepData.currentStep ?? 0] ?? ''}
76+
onChange={(val) => handleOptionChange(msg.stepData?.currentStep ?? 0, val)}
77+
disabled={currentStep !== undefined && currentStep > msg.stepData.currentStep!}
7978
/>
80-
)}
81-
{/* {children} */}
79+
) : null}
8280
</div>
83-
{msg.type === 'CARD_LIST' && msg.recommendations?.length ? (
84-
<ul className="inline-grid grid-cols-1 mt-5 sm:grid-cols-3 gap-2 justify-start">
85-
{msg.recommendations.map((rec) => (
86-
<li key={rec.cocktailId}>
87-
<BotCocktailCard
88-
cocktailId={rec.cocktailId}
89-
cocktailName={rec.cocktailName}
90-
cocktailNameKo={rec.cocktailNameKo}
91-
cocktailImgUrl={rec.cocktailImgUrl}
92-
alcoholStrength={rec.alcoholStrength}
81+
{msg.type === 'CARD_LIST' && msg.stepData?.recommendations?.length ? (
82+
<>
83+
{/* 카드 목록 */}
84+
<ul className="inline-grid grid-cols-1 mt-5 sm:grid-cols-3 gap-2 justify-start">
85+
{msg.stepData.recommendations.map((rec) => (
86+
<li key={rec.cocktailId}>
87+
<BotCocktailCard
88+
cocktailId={rec.cocktailId}
89+
cocktailName={rec.cocktailName}
90+
cocktailNameKo={rec.cocktailNameKo}
91+
cocktailImgUrl={rec.cocktailImgUrl}
92+
alcoholStrength={rec.alcoholStrength}
93+
/>
94+
</li>
95+
))}
96+
</ul>
97+
98+
{/* 카드 목록 마지막 restart */}
99+
{msg.stepData?.options && msg.stepData.options?.length > 0 && (
100+
<div className="flex flex-col w-fit max-w-[80%] min-w-[120px] p-3 rounded-2xl rounded-tl-none bg-white text-black opacity-0 animate-fadeIn mt-3">
101+
<p>다시 추천받기를 원하시나요?</p>
102+
<BotOptions
103+
options={msg.stepData.options}
104+
step={msg.stepData.currentStep ?? 0}
105+
value={selectedOptions[msg.stepData.currentStep ?? 0] ?? ''}
106+
onChange={(val) => handleOptionChange(msg.stepData?.currentStep ?? 0, val)}
107+
disabled={currentStep ? currentStep > (msg.stepData.currentStep ?? 0) : false}
93108
/>
94-
</li>
95-
))}
96-
</ul>
109+
</div>
110+
)}
111+
</>
97112
) : (
98113
''
99114
)}

src/domains/recommend/components/bot/BotOptions.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ interface BotOptionsProps {
88
value: string;
99
onChange: (value: string) => void;
1010
step: number;
11-
currentStep: number;
11+
currentStep?: number;
12+
disabled?: boolean;
1213
}
1314

14-
function BotOptions({ options, value, onChange, step, currentStep }: BotOptionsProps) {
15+
function BotOptions({ options, value, onChange, step, disabled = false }: BotOptionsProps) {
1516
return (
1617
<div role="radiogroup" className="flex flex-col gap-3 mt-5">
1718
{options.map((opt) => (
@@ -27,7 +28,7 @@ function BotOptions({ options, value, onChange, step, currentStep }: BotOptionsP
2728
value={opt.value}
2829
checked={value === opt.value}
2930
onChange={() => onChange(opt.value)}
30-
disabled={currentStep > step && value !== opt.value}
31+
disabled={disabled}
3132
className="sr-only"
3233
/>
3334
<span
@@ -37,9 +38,9 @@ function BotOptions({ options, value, onChange, step, currentStep }: BotOptionsP
3738
? 'bg-secondary shadow-[inset_0_0_6px_rgba(255,196,1,1)]'
3839
: 'bg-gray-light'
3940
}
40-
${currentStep > step && value !== opt.value ? 'cursor-not-allowed bg-gray-light' : 'hover:bg-secondary'}`}
41+
${disabled ? 'cursor-not-allowed bg-gray-light' : 'hover:bg-secondary'}`}
4142
>
42-
<span>{opt.label}</span>
43+
{opt.label}
4344
</span>
4445
</label>
4546
))}

0 commit comments

Comments
 (0)