CHATBOT 챗봇 살펴보기

Kimmy·2025년 5월 8일

PWA_PROJECT

목록 보기
27/47
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>}
  • 버튼을 누르면 isOpentrue로 바꿔 채팅창이 열림
  • × 버튼 누르면 다시 닫힘

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 안내 문구자주 묻는 질문 펼치기
✅ 질문 버튼들클릭 시 자동 메시지 전송
🗨️ 메시지 영역사용자/봇 메시지 나눠서 렌더링
⌨️ 입력창 + 전송 버튼메시지 입력 및 전송 처리
profile
바리바리 개바리 🌼

0개의 댓글