Skip to content

Commit 545958d

Browse files
authored
Merge pull request #83 from geulDa/feat/#69/chatbotpage
✨Feat: 챗봇페이지 제작
2 parents b1858b4 + 37a0c0d commit 545958d

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import { cn } from '@/shared/lib';
4+
import { cva, type VariantProps } from 'class-variance-authority';
5+
6+
const chatBubbleStyle = cva(
7+
`
8+
flex items-center
9+
px-[1.9rem] py-[1.3rem]
10+
rounded-[2rem]
11+
text-label-lg foreground
12+
break-words
13+
w-fit max-w-[80%]
14+
`,
15+
{
16+
variants: {
17+
variant: {
18+
received: 'bg-gray-100 self-start',
19+
sent: 'bg-mint-100 self-end',
20+
},
21+
},
22+
defaultVariants: {
23+
variant: 'received',
24+
},
25+
},
26+
);
27+
28+
interface ChattingProps extends VariantProps<typeof chatBubbleStyle> {
29+
message: string;
30+
}
31+
32+
export default function Chatting({ message, variant }: ChattingProps) {
33+
return (
34+
<div className='w-full flex flex-col'>
35+
<div className={cn(chatBubbleStyle({ variant }))}>{message}</div>
36+
</div>
37+
);
38+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client';
2+
import { Icon } from '@/shared/icons';
3+
import { cn } from '@/shared/lib';
4+
import { cva } from 'class-variance-authority';
5+
import { useChattingInput } from '@/shared/hooks/useChattingInput';
6+
7+
const inputWrapperStyle = cva(
8+
'flex items-center justify-between w-full bg-gray-100 px-[0.6rem] py-[0.7rem] rounded-[2rem]',
9+
);
10+
11+
interface ChattingInputProps {
12+
onSend?: (text: string) => void;
13+
}
14+
15+
16+
export default function ChattingInput({ onSend }: ChattingInputProps) {
17+
const {
18+
message,
19+
setMessage,
20+
inputRef,
21+
handleSubmit,
22+
handleKeyDown,
23+
} = useChattingInput({ onSend });
24+
25+
return (
26+
<div
27+
className={cn(
28+
'fixed bottom-0 left-1/2 -translate-x-1/2 w-full bg-gray-100 px-[0.6rem] py-[0.7rem] flex items-center gap-[0.8rem]',
29+
)}
30+
>
31+
<div
32+
className={cn(
33+
inputWrapperStyle(),
34+
'flex-1 h-[4rem] bg-white border border-gray-200 rounded-[2rem] flex items-center pl-[1.4rem] pr-[1.2rem] py-[1rem]',
35+
)}
36+
>
37+
<input
38+
ref={inputRef}
39+
value={message}
40+
onChange={(e) => setMessage(e.target.value)}
41+
onKeyDown={handleKeyDown}
42+
type='text'
43+
placeholder='무엇이든 물어보세요'
44+
className='w-full bg-transparent outline-none text-label-lg placeholder:text-gray-300 text-gray-900'
45+
/>
46+
</div>
47+
48+
<button
49+
type='button'
50+
onClick={handleSubmit}
51+
className={cn(
52+
'w-[4rem] h-[4rem] flex justify-center items-center rounded-[2rem] bg-mint-500 flex-shrink-0',
53+
)}
54+
>
55+
<Icon name='backto' size={20} color='gray-50' rotate={90} />
56+
</button>
57+
</div>
58+
);
59+
}

src/pages/chatbot/index.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use client';
2+
3+
import { useState, useEffect, useRef } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { cva } from 'class-variance-authority';
6+
import { cn } from '@/shared/lib';
7+
import { Header } from '@/shared/components';
8+
import Chatting from '@/pages/chatbot/components/ChattingBubble';
9+
import ChattingInput from '@/pages/chatbot/components/ChattingInput';
10+
11+
const chatPageStyle = cva(
12+
'relative w-full h-dvh overflow-hidden bg-white flex flex-col',
13+
);
14+
15+
const mainStyle = cva(
16+
'relative w-full flex-1 pt-[14.4rem] pb-[10rem] px-[2.4rem] overflow-auto flex flex-col gap-[0.6rem]',
17+
);
18+
19+
const introStyle = cva('flex flex-col items-start gap-[1rem]');
20+
21+
export default function ChatPage() {
22+
const router = useRouter();
23+
const bottomRef = useRef<HTMLDivElement>(null);
24+
25+
type Message = {
26+
id: number;
27+
text: string;
28+
variant: 'received' | 'sent';
29+
};
30+
31+
const [messages, setMessages] = useState<Message[]>([]);
32+
33+
// 새로운 채팅 자동 스크롤
34+
useEffect(() => {
35+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
36+
}, [messages]);
37+
38+
// 메세지 전송 핸들러
39+
const handleSend = (text: string) => {
40+
if (!text.trim()) return;
41+
const newMsg = { id: Date.now(), text, variant: 'sent' as const };
42+
setMessages((prev) => [...prev, newMsg]);
43+
};
44+
45+
return (
46+
<div className={cn(chatPageStyle())}>
47+
{/* 헤더 고정 */}
48+
<div className='fixed top-0 left-0 w-full z-10'>
49+
<Header title='ChatBot' onClick={() => router.back()} />
50+
</div>
51+
52+
{/* 메인 콘텐츠 */}
53+
<main className={cn(mainStyle())}>
54+
{/* 로고 + 기본 멘트 */}
55+
<div className={cn(introStyle())}>
56+
{/* 로고 자리 (임시) */}
57+
<div className='w-[6rem] h-[6rem] rounded-full bg-gray-200 flex-shrink-0' />
58+
<Chatting
59+
message='안녕하세요, 글다에요! 부천시 여행에 대한 정보를 쉽게 알려드릴게요.'
60+
variant='received'
61+
/>
62+
<Chatting
63+
message='원하시는 정보를 물어봐주세요!'
64+
variant='received'
65+
/>
66+
</div>
67+
68+
{/* 사용자 메시지 */}
69+
{messages
70+
.filter((msg) => msg.id > 2)
71+
.map((msg) => (
72+
<Chatting key={msg.id} message={msg.text} variant={msg.variant} />
73+
))}
74+
75+
{/* 스크롤 */}
76+
<div ref={bottomRef} />
77+
</main>
78+
79+
{/* 입력창 */}
80+
<ChattingInput onSend={handleSend} />
81+
</div>
82+
);
83+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use client';
2+
import { useEffect, useRef, useState } from 'react';
3+
4+
interface UseChattingInputProps {
5+
onSend?: (text: string) => void;
6+
}
7+
8+
export const useChattingInput = ({ onSend }: UseChattingInputProps) => {
9+
const [message, setMessage] = useState('');
10+
const inputRef = useRef<HTMLInputElement>(null);
11+
12+
// focus 시 스크롤 중앙으로
13+
useEffect(() => {
14+
const handleFocus = () => {
15+
setTimeout(() => {
16+
inputRef.current?.scrollIntoView({
17+
behavior: 'smooth',
18+
block: 'center',
19+
});
20+
}, 200);
21+
};
22+
const el = inputRef.current;
23+
el?.addEventListener('focus', handleFocus);
24+
return () => el?.removeEventListener('focus', handleFocus);
25+
}, []);
26+
27+
// 메시지 전송
28+
const handleSubmit = () => {
29+
if (!message.trim()) return;
30+
onSend?.(message);
31+
setMessage('');
32+
};
33+
34+
// 엔터키 전송
35+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
36+
if (e.key === 'Enter' && !e.shiftKey) {
37+
e.preventDefault();
38+
handleSubmit();
39+
}
40+
};
41+
42+
return {
43+
message,
44+
setMessage,
45+
inputRef,
46+
handleSubmit,
47+
handleKeyDown,
48+
};
49+
};

0 commit comments

Comments
 (0)