import { useState, useRef, useEffect } from "react";
export default function ChatWidget() {
const [isOpen, setIsOpen] = useState(false); // 채팅창 열기/닫기 상태
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [isFAQOpen, setIsFAQOpen] = useState(false); // 자주하는 문의창 열기/닫기 상태
const messagesEndRef = useRef(null);
const chatWidgetRef = useRef(null); // 채팅창을 감싸는 div 참조
const exampleQuestions = [
"시술 소요 시간은 얼마나 되나요?",
"업체에 바로 전화 연결할 수 있나요?",
"현재 진행 중인 이벤트가 있나요?",
"1대1 상담을 원합니다.",
];
const botResponses = {
"시술 소요 시간은 얼마나 되나요?": "시술에 따라 다르지만 평균적으로 30분에서 1시간 정도 소요됩니다 💆♀️",
"업체에 바로 전화 연결할 수 있나요?": "네! 각 업체 상세 페이지에서 전화 연결 버튼을 누르시면 바로 통화 가능합니다 📞",
"현재 진행 중인 이벤트가 있나요?": "지금은 봄맞이 할인 이벤트 중이에요! '이벤트' 탭에서 자세히 확인해보세요 🌸",
"1대1 상담을 원합니다.": "잠시만 기다려주세요. 상담사와 곧 연결해드리겠습니다. 📝",
};
const handleSendMessage = () => {
const trimmedInput = input.trim();
if (!trimmedInput) return;
setMessages((prev) => [...prev, { sender: "user", text: trimmedInput }]);
setInput("");
setTimeout(() => {
const response =
botResponses[trimmedInput] || "안녕하세요! 무엇을 도와드릴까요? 🤖";
setMessages((prev) => [...prev, { sender: "bot", text: response }]);
}, 800);
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
// 채팅창 외부를 클릭하면 자주 묻는 질문을 접음
useEffect(() => {
const handleClickOutside = (e) => {
if (chatWidgetRef.current && !chatWidgetRef.current.contains(e.target)) {
setIsFAQOpen(false); // 자주 묻는 질문 접기
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<div className="fixed bottom-4 right-4 z-50" ref={chatWidgetRef}>
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
className="bg-blue-500 hover:bg-blue-600 text-white w-16 h-16 rounded-full shadow-xl flex items-center justify-center aspect-square transition duration-200 transform hover:scale-110"
>
💬
</button>
)}
{/* 채팅창이 열렸을 때 */}
{isOpen && (
<div className="bg-white shadow-lg rounded-xl w-80 h-[60vh] flex flex-col border border-gray-200 font-sans">
{/* 문의하기 섹션 */}
<div className="bg-blue-100 text-gray-800 p-4 rounded-t-xl flex justify-between items-center">
<h3 className="text-lg font-semibold">문의하기</h3>
{/* X 버튼 추가 */}
<button
onClick={() => setIsOpen(false)}
className="text-gray-800 text-xl font-semibold hover:text-blue-700"
>
×
</button>
</div>
{/* 자주 묻는 질문 섹션 열기/닫기 */}
<div className="p-4 text-center cursor-pointer text-blue-700" onClick={() => setIsFAQOpen(!isFAQOpen)}>
{isFAQOpen ? "질문을 클릭해주세요!" : "자주 묻는 질문을 확인해보세요!"}
</div>
{/* 자주 묻는 질문이 열리면 질문 목록 보여주기 */}
{isFAQOpen && (
<div className="flex flex-col justify-center items-center gap-3 mt-2 mb-4">
{exampleQuestions.map((q, i) => (
<button
key={i}
onClick={() => {
setInput(q);
setTimeout(() => {
setMessages((prev) => [
...prev,
{ sender: "user", text: q },
{ sender: "bot", text: botResponses[q] },
]);
}, 100);
setIsFAQOpen(false); // 질문 클릭 시 다시 자주 묻는 질문 목록으로 돌아가기
}}
className="text-sm bg-blue-100 hover:bg-blue-200 text-blue-800 px-3 py-1 rounded-full transition duration-200"
>
{q}
</button>
))}
</div>
)}
{/* 자주 묻는 질문 섹션 */}
<div className="flex-1 p-4 overflow-y-auto bg-gray-50 space-y-4">
{messages.map((msg, index) => (
<div
key={index}
className={`flex ${
msg.sender === "user" ? "justify-end" : "justify-start"
}`}
>
<span
className={`inline-block px-4 py-2 text-base rounded-lg max-w-[75%] ${
msg.sender === "user"
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-900"
}`}
>
{msg.text}
</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="py-2 px-3 flex gap-2 bg-white border-t">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="메시지를 입력하세요"
className="flex-1 p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-400"
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.nativeEvent.isComposing
) {
e.preventDefault();
handleSendMessage();
}
}}
/>
<button
onClick={handleSendMessage}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-3 rounded-md"
>
전송
</button>
</div>
</div>
)}
</div>
);
}```
## ✅ 이 코드의 주요 학습 포인트
- `useRef`로 DOM 참조 및 외부 클릭 감지
- `useEffect`로 동작 타이밍 제어
- `useState`로 UI 상태 제어
- map으로 동적 버튼 렌더링
- UI/UX 배려: 스크롤 자동, FAQ 닫기 등
## 🔍 주요 기능 설명
### ✅ 상태 관리 (`useState`)
- `isOpen`: 채팅창 열림/닫힘 여부 (`false` → 닫힘 상태)
- `messages`: 채팅 메시지 목록 (유저 & 봇의 대화 저장)
- `input`: 입력창의 현재 텍스트
- `isFAQOpen`: 자주 묻는 질문(F.A.Q) 목록 열림/닫힘 상태
---
### ✅ 참조 관리 (`useRef`)
- `messagesEndRef`: 채팅창 스크롤을 맨 아래로 내릴 때 사용
- `chatWidgetRef`: 채팅창 전체를 감싸는 div 참조 (외부 클릭 감지에 사용)
---
## 🧠 핵심 기능별 설명
### 1. **채팅창 열기/닫기**
```tsx
{!isOpen && <button onClick={() => setIsOpen(true)}>💬</button>}
- 버튼을 누르면
isOpen을 true로 바꿔 채팅창이 열림
× 버튼 누르면 다시 닫힘
2. FAQ 열기/닫기
<div onClick={() => setIsFAQOpen(!isFAQOpen)}>
- 클릭 시 FAQ 목록을 열거나 닫음
- 질문 클릭 시 자동으로 메시지 입력 → 챗봇 응답 생성
3. 채팅 메시지 전송
const handleSendMessage = () => {
...
setMessages(...);
setTimeout(() => {
setMessages(...);
}, 800);
};
- 사용자가 메시지를 입력하면 메시지를 추가하고,
- 0.8초 후에 봇 응답(
botResponses)을 찾아 자동 응답함
4. 스크롤 맨 아래로 이동
useEffect(() => {
scrollToBottom();
}, [messages])
- 메시지가 추가될 때마다
ref를 이용해 스크롤을 맨 아래로 이동
5. 채팅창 외부 클릭 시 FAQ 닫기
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", ...);
}, []);
- 외부 클릭 시
chatWidgetRef 범위 밖이면 FAQ를 자동 닫음
💬 UI 구성 요약
| 영역 | 기능 |
|---|
| 🔘 채팅 아이콘 버튼 | 채팅창 열기 |
| 🧾 제목 + X버튼 | 문의하기 헤더 및 닫기 버튼 |
| ❓ FAQ 안내 문구 | 자주 묻는 질문 펼치기 |
| ✅ 질문 버튼들 | 클릭 시 자동 메시지 전송 |
| 🗨️ 메시지 영역 | 사용자/봇 메시지 나눠서 렌더링 |
| ⌨️ 입력창 + 전송 버튼 | 메시지 입력 및 전송 처리 |