블로그워드프레스에 AI 고객지원 챗봇 만들기 (5) — 대화 로그, 문의 접수, 운영 팁

워드프레스에 AI 고객지원 챗봇 만들기 (5) — 대화 로그, 문의 접수, 운영 팁

챗봇을 만들었으면 끝일까? 아니다. 운영이 시작이다. 사용자가 실제로 뭘 물어보는지 모르면 챗봇을 개선할 수 없다. 로그가 없으면 눈감고 운전하는 거다. 이번 글에서는 대화 로그 시스템, 문의 접수 기능, 그리고 몇 달 운영하면서 쌓인 실전 팁을 정리한다.

대화 로그 DB 테이블 설계

별도의 커스텀 테이블을 만든다. 워드프레스의 postmeta나 options 테이블에 로그를 쑤셔넣는 건 안 된다. 데이터가 쌓이면 사이트 전체가 느려진다.

function nalkkul_chatbot_create_log_table() {
    global $wpdb;
    $table = $wpdb->prefix . 'chatbot_logs';
    if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table) return;

    $charset = $wpdb->get_charset_collate();
    $wpdb->query("CREATE TABLE {$table} (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_message TEXT NOT NULL,
        bot_response TEXT NOT NULL,
        response_type VARCHAR(20) DEFAULT 'rule',
        ip_address VARCHAR(45) DEFAULT '',
        user_id BIGINT UNSIGNED DEFAULT 0,
        response_time FLOAT DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    ) {$charset};");
}
add_action('init', 'nalkkul_chatbot_create_log_table');

init 액션에 걸어뒀지만, 테이블이 이미 존재하면 바로 리턴한다. 매 페이지 로드마다 SHOW TABLES 쿼리가 실행되는 게 부담이면 옵션 값으로 플래그를 관리할 수도 있다. 하지만 SHOW TABLES는 충분히 빠르니까 이 정도로 충분하다.

테이블 컬럼 설계를 보면:

  • user_message (TEXT) — 사용자가 입력한 원본 메시지
  • bot_response (TEXT) — 챗봇의 응답. 규칙 기반이든 AI든 검색이든 전부 저장
  • response_type (VARCHAR 20) — ‘rule’, ‘ai’, ‘search’, ‘error’ 네 가지. 어떤 엔진이 응답했는지 구분
  • ip_address (VARCHAR 45) — IPv6까지 고려해서 45자. IPv4만 쓰면 15자로 충분하지만 미래 대비
  • user_id — 로그인한 회원이면 ID 저장. 비회원은 0
  • response_time (FLOAT) — AI 응답의 소요 시간(초 단위). 성능 모니터링용
  • created_at — 자동 타임스탬프

로그 저장 함수

모든 대화를 기록하는 중앙 함수. AI 응답이든 규칙 기반이든 검색이든 에러든 전부 이 함수를 통해 저장한다.

function nalkkul_chatbot_log($question, $answer, $type = 'rule', $time = 0) {
    global $wpdb;
    $wpdb->insert($wpdb->prefix . 'chatbot_logs', [
        'user_message'  => mb_substr($question, 0, 1000),
        'bot_response'  => mb_substr($answer, 0, 2000),
        'response_type' => $type,
        'ip_address'    => sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? ''),
        'user_id'       => get_current_user_id(),
        'response_time' => $time,
        'created_at'    => current_time('mysql'),
    ]);
}

mb_substr로 길이를 제한하는 이유: TEXT 타입이라 이론적으로 무한히 저장할 수 있지만, 누군가 악의적으로 엄청 긴 메시지를 보낼 수 있다. 질문은 1000자, 답변은 2000자로 잘라낸다.

규칙 기반 응답은 JS에서 AJAX로 로그를 보낸다:

add_action('wp_ajax_nalkkul_chatbot_log', 'nalkkul_chatbot_log_ajax');
add_action('wp_ajax_nopriv_nalkkul_chatbot_log', 'nalkkul_chatbot_log_ajax');

function nalkkul_chatbot_log_ajax() {
    check_ajax_referer('nalkkul_chatbot_nonce', 'nonce');
    $q = sanitize_textarea_field($_POST['question'] ?? '');
    $a = sanitize_textarea_field($_POST['answer'] ?? '');
    if (!empty($q)) {
        nalkkul_chatbot_log($q, $a, 'rule');
    }
    wp_send_json_success();
}

AI 응답은 서버에서 바로 저장하지만, 규칙 기반 응답은 클라이언트(JS)에서 처리되니까 별도의 AJAX 엔드포인트가 필요하다. wp_ajax_nopriv_ 접두사는 비로그인 사용자도 사용 가능하게 한다.

관리자 로그 페이지

워드프레스 관리자 메뉴에 “챗봇 로그” 페이지를 추가한다. 여기서 대화 기록을 확인하고, 필터링하고, CSV로 내보내고, 삭제할 수 있다.

add_action('admin_menu', function() {
    add_menu_page(
        '챗봇 대화 로그',
        '💬 챗봇 로그',
        'manage_options',
        'chatbot-logs',
        'nalkkul_chatbot_logs_page',
        'dashicons-format-chat',
        58
    );
});

로그 페이지 상단에 통계 카드 5개가 나온다: 전체 대화, 오늘, AI 응답, 규칙 기반, 오류. 한눈에 챗봇 상태를 파악할 수 있다.

$stats_total = $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
$stats_ai = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'ai'");
$stats_rule = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'rule'");
$stats_error = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE response_type = 'error'");
$stats_today = $wpdb->get_var($wpdb->prepare(
    "SELECT COUNT(*) FROM {$table} WHERE DATE(created_at) = %s", 
    current_time('Y-m-d')
));

AJAX로 동적 로딩 — 캐시 문제 해결

처음에는 PHP에서 로그를 직접 렌더링했다. 문제가 있었다. 워드프레스 관리자 페이지가 캐시되는 경우가 있어서 새 대화가 안 보였다. 페이지를 새로고침해도 이전 데이터가 나오는 경우도 있었다.

해결: 페이지 로드 시 AJAX로 로그를 가져오도록 바꿨다. 브라우저 포커스를 받을 때도 자동 갱신.

(function(){
    var nonce = '';
    var ajaxUrl = '';

    function loadLogs() {
        var params = new URLSearchParams(window.location.search);
        var fd = new FormData();
        fd.append('action', 'nalkkul_chatbot_get_logs');
        fd.append('nonce', nonce);
        fd.append('s', params.get('s') || '');
        fd.append('type', params.get('type') || '');
        fd.append('paged', params.get('paged') || '1');

        fetch(ajaxUrl, {method:'POST', body:fd})
        .then(function(r){return r.json();})
        .then(function(res){
            if(res.success) {
                document.getElementById('tg-logs-tbody').innerHTML = res.data.html;
                var s = res.data.stats;
                var cards = document.querySelectorAll('[data-stat]');
                cards.forEach(function(c){
                    var key = c.getAttribute('data-stat');
                    if(s[key] !== undefined) c.textContent = Number(s[key]).toLocaleString();
                });
            }
        });
    }

    loadLogs();
    window.addEventListener('focus', loadLogs);
})();

window.addEventListener('focus', loadLogs)가 핵심 트릭이다. 관리자가 챗봇 로그 탭을 열어놓고 다른 탭에서 작업하다가 돌아오면 자동으로 최신 로그를 보여준다. 새로고침 필요 없다.

서버측 AJAX 핸들러에서는 HTML을 직접 생성해서 리턴한다:

function nalkkul_chatbot_get_logs_ajax() {
    if (!current_user_can('manage_options')) wp_send_json_error();
    check_ajax_referer('chatbot_logs_ajax', 'nonce');

    global $wpdb;
    $table = $wpdb->prefix . 'chatbot_logs';

    // 필터, 페이징 처리
    $type_filter = sanitize_text_field($_POST['type'] ?? '');
    $search = sanitize_text_field($_POST['s'] ?? '');
    $page = max(1, intval($_POST['paged'] ?? 1));
    $per_page = 30;
    $offset = ($page - 1) * $per_page;

    $where = "WHERE 1=1";
    if ($type_filter) $where .= $wpdb->prepare(" AND response_type = %s", $type_filter);
    if ($search) $where .= $wpdb->prepare(
        " AND (user_message LIKE %s OR bot_response LIKE %s)", 
        "%{$search}%", "%{$search}%"
    );

    $logs = $wpdb->get_results(
        "SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT {$per_page} OFFSET {$offset}"
    );

    // 응답 유형별 배지 색상
    $badge_colors = ['ai' => '#9b59b6', 'rule' => '#27ae60', 'error' => '#e74c3c', 'search' => '#3498db'];
    $badge_labels = ['ai' => 'AI', 'rule' => '규칙', 'error' => '오류', 'search' => '검색'];

    // HTML 생성 + 통계 포함
    wp_send_json_success(['html' => $html, 'stats' => $stats]);
}

응답 유형별로 색이 다른 배지를 붙여서 한눈에 구분할 수 있다. 보라색은 AI, 초록색은 규칙 기반, 빨간색은 오류, 파란색은 검색.

CSV 내보내기

로그를 엑셀에서 분석하고 싶을 때 CSV로 내보낸다. 한글 깨짐을 방지하기 위해 UTF-8 BOM을 파일 시작에 넣는다.

if (isset($_POST['export_csv']) && check_admin_referer('chatbot_logs_action')) {
    $rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC", ARRAY_A);
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename=chatbot_logs_' . date('Ymd') . '.csv');
    $out = fopen('php://output', 'w');
    fwrite($out, "\xEF\xBB\xBF"); // UTF-8 BOM — 엑셀 한글 깨짐 방지
    fputcsv($out, ['ID', '질문', '답변', '유형', 'IP', '회원ID', '응답시간', '날짜']);
    foreach ($rows as $r) {
        fputcsv($out, $r);
    }
    fclose($out);
    exit;
}

\xEF\xBB\xBF — 이 3바이트가 없으면 엑셀에서 CSV를 열었을 때 한글이 깨진다. UTF-8 BOM(Byte Order Mark)을 파일 앞에 넣어서 엑셀에게 “이 파일은 UTF-8이야”라고 알려준다. 사소하지만 빠뜨리면 짜증나는 부분.

문의 접수 기능 — 인라인 폼

사용자가 “문의”라고 입력하면 채팅 창 안에 문의 폼이 나타난다. 이름, 이메일, 메시지를 입력하고 보내면 관리자 이메일로 전달된다. 별도의 문의 페이지로 이동할 필요 없다.

function nalkkul_chatbot_contact() {
    check_ajax_referer('nalkkul_chatbot_nonce', 'nonce');

    $name    = sanitize_text_field($_POST['name'] ?? '');
    $email   = sanitize_email($_POST['email'] ?? '');
    $message = sanitize_textarea_field($_POST['message'] ?? '');

    if (empty($name) || empty($email) || empty($message)) {
        wp_send_json_error(['message' => '모든 항목을 입력해주세요.']);
    }

    $to      = '[email protected]';
    $subject = "[무엇이든알아보자] 챗봇 문의 - {$name}님";
    $body    = "이름: {$name}\n이메일: {$email}\n\n문의 내용:\n{$message}";
    $headers = ['Reply-To: ' . $email];

    $sent = wp_mail($to, $subject, $body, $headers);

    if ($sent) {
        wp_send_json_success(['message' => '문의가 접수되었습니다!']);
    } else {
        wp_send_json_error(['message' => '전송에 실패했습니다. 잠시 후 다시 시도해주세요.']);
    }
}

Reply-To 헤더를 설정해서 관리자가 메일에서 바로 답장할 수 있게 했다. sanitize_email()로 이메일 형식을 검증하고, sanitize_textarea_field()로 HTML 태그를 제거한다. nonce 검증은 CSRF 공격을 막는다.

프론트엔드에서 폼을 동적으로 생성하는 JS:

function submitContactForm(formEl) {
    var nameEl  = formEl.querySelector('.tg-cf-name');
    var emailEl = formEl.querySelector('.tg-cf-email');
    var msgEl   = formEl.querySelector('.tg-cf-msg');
    var btn     = formEl.querySelector('.tg-cf-submit');

    var name  = nameEl.value.trim();
    var email = emailEl.value.trim();
    var msg   = msgEl.value.trim();

    if (!name || !email || !msg) {
        addMessage("모든 항목을 입력해주세요! 📝", 'bot');
        return;
    }

    btn.disabled = true;
    btn.textContent = '전송 중...';

    var formData = new FormData();
    formData.append('action', 'nalkkul_chatbot_contact');
    formData.append('nonce', NONCE);
    formData.append('name', name);
    formData.append('email', email);
    formData.append('message', msg);

    fetch(AJAX_URL, { method: 'POST', body: formData })
    .then(function(r) { return r.json(); })
    .then(function(data) {
        if (data.success) {
            addMessage("문의가 접수되었습니다! 빠른 시일 내에 답변 드리겠습니다. 📬", 'bot');
            nameEl.value = '';
            emailEl.value = '';
            msgEl.value = '';
        } else {
            addMessage(data.data.message || "전송에 실패했습니다. 😢", 'bot');
        }
        btn.disabled = false;
        btn.textContent = '📧 문의 보내기';
    });
}

전송 중에 버튼을 비활성화해서 중복 전송을 방지한다. 전송 완료 후 입력 필드를 초기화하고 버튼을 다시 활성화한다.

보안 체크리스트

챗봇은 외부에 노출된 엔드포인트다. 보안에 신경 써야 할 부분들:

  1. Nonce 검증 — 모든 AJAX 핸들러에서 check_ajax_referer()를 호출한다. CSRF 공격을 막는 기본 중의 기본.
  2. Rate Limiting — IP당 시간당 20회. Transient API로 구현. DDoS 방어까지는 안 되지만 일반적인 남용은 막을 수 있다.
  3. 입력 정화sanitize_textarea_field(), sanitize_text_field(), sanitize_email(). 모든 사용자 입력에 적용.
  4. 출력 이스케이프escapeHtml()으로 XSS 방지. HTML을 직접 innerHTML에 넣기 전에 반드시 이스케이프.
  5. 관리자 권한 확인 — 로그 조회 AJAX에서 current_user_can('manage_options') 체크.
  6. SQL Injection 방지$wpdb->prepare()를 사용. 직접 문자열 연결 금지.

운영 팁 — 몇 달 써보고 깨달은 것들

1. 로그를 주기적으로 분석하라

가장 자주 묻는 질문 TOP 10을 뽑아보면 패턴이 보인다. “로또 번호” 검색이 많으면 knowledgeBase에 로또 카테고리를 추가한다. “○○ 어떻게 해?” 같은 질문이 많으면 해당 서비스의 안내 문구를 개선한다. 로그가 곧 사용자 리서치다.

2. 면책 문구 처리

AI가 생성한 답변에는 부정확한 정보가 포함될 수 있다. 특히 건강, 법률, 금융 관련 질문에 AI가 답변하면 문제가 될 수 있다. 시스템 프롬프트에 “모르는 건 검색엔진을 이용하라고 안내하세요”를 넣어둔 이유다. 필요하면 AI 응답 하단에 면책 문구를 자동으로 붙이는 것도 방법이다.

3. AI 모델 선택 가이드

상황추천 구성월 비용
비용 0원으로 시작규칙 기반만 (AI_ENGINE 미설정)0원
품질보다 비용 우선Ollama + qwen2.5:3b전기세만
균형잡힌 선택Ollama + qwen2.5:14b전기세만
품질 최우선Claude Haiku API~$5/월
최고급Claude Sonnet API~$20/월

4. 성능 모니터링

response_time 컬럼으로 AI 응답 시간을 추적할 수 있다. 평균이 10초를 넘기면 모델을 더 작은 것으로 바꾸거나, PC를 업그레이드해야 한다는 신호다. 사용자가 10초 이상 기다리면 이탈률이 급격히 올라간다.

전체 아키텍처 정리

5편에 걸쳐서 구현한 챗봇의 전체 흐름을 정리하면:

사용자 메시지 입력
    │
    ▼
[JS] findResponse() — 규칙 기반 키워드 매칭
    │
    ├── 매칭 성공 → 즉시 응답 (0.1초) + 로그 저장
    │
    └── 매칭 실패 → AJAX 서버 전송
                       │
                       ▼
              [PHP] nalkkul_chatbot_ai()
                       │
                       ├── Rate Limit 체크 (IP당 20회/시간)
                       │
                       ▼
              [PHP] nalkkul_chatbot_search_formatted() — DB 검색
                       │
                       ├── 검색 결과 있음 → 직접 포맷 후 응답 (0.3초)
                       │
                       └── 검색 결과 없음 → AI 호출
                                              │
                                              ├── Ollama (로컬, 3~15초)
                                              └── Claude (API, 1~3초)
                                              │
                                              ▼
                                        응답 + 로그 저장
                                              │
                                              ▼
                                    [JS] 메시지 표시 + 응답시간 표시

파일 하나(chatbot.php)에 모든 것이 담겨 있다. PHP 함수, CSS 스타일, JS 로직. 외부 의존성 없다. wp-content/mu-plugins/에 넣으면 끝이다.

앞으로의 계획

현재 구현에서 개선하고 싶은 것들:

  • 스트리밍 응답 — 지금은 AI가 전체 답변을 만들 때까지 기다린다. Server-Sent Events로 토큰 단위로 스트리밍하면 체감 속도가 빨라진다.
  • 벡터 검색 — MySQL LIKE 검색의 한계가 있다. “재밌는 거”로 “이상형 월드컵”을 찾지 못한다. 임베딩 기반 의미 검색을 붙이면 관련도가 올라간다.
  • 사용자 피드백 — 봇 응답마다 “도움됨/안됨” 버튼을 달아서 자동으로 knowledgeBase를 개선하는 파이프라인.
  • 다국어 지원 — 영어 질문도 처리할 수 있게. AI 엔진이 이미 다국어를 지원하니 UI만 바꾸면 된다.

챗봇은 한번 만들면 끝나는 프로젝트가 아니다. 로그를 보면서 계속 키워드를 추가하고, 응답을 다듬고, 모델을 교체하는 반복 작업이다. 하지만 그 과정에서 사용자가 사이트를 어떻게 쓰는지 정말 많이 배운다. 분석 도구 열 개보다 챗봇 로그 하나가 더 많은 걸 알려준다.


이 시리즈의 다른 글

이 글이 도움이 되셨다면 공유해주세요!

댓글 남기기

무엇이든 물어보세요! 💬