고객 문의 자동 응답 시스템에서 문자(SMS) 알림 추가 구현

📅 소개

지난번에는 고객이 구글폼을 통해 문의를 남기면, AI를 활용해 자동으로 이메일 응답을 생성하는 시스템을 구축했습니다.

이번에는 한 단계 더 나아가, 이메일이 전송되었음을 고객에게 즉시 문자(SMS)로 알리는 기능을 추가 구현했습니다.

이를 통해 고객은 문의가 정상적으로 처리되었는지 실시간으로 확인할 수 있으며, 이메일을 놓치는 경우에도 중요한 정보를 즉시 확인할 수 있습니다.

한국어라는 단어가있는 배너

전화에 두 개��의 한국 문자 메시지

🔧 구현 과정

✅ 사용 도구

  • Google Apps Script (GAS): Google Forms와 스프레드시트에서 데이터를 처리

  • 뿌리오 API: 문자(SMS) 발송을 위해 활용

  • ChatGPT API: 고객 문의 자동 답변 생성

한국의 흐름도
한국어로 메시지를 보내는 과정을 보여주는 다이어그램

📝 전체 흐름

  1. 고객이 Google Forms를 통해 문의를 제출

  2. Google Apps Script가 자동 실행되어 문의 데이터를 처리

  3. ChatGPT를 활용해 문의에 대한 답변을 생성

  4. 이메일을 통해 고객에게 답변 전송

  5. 뿌리오 API를 사용하여 고객에게 "이메일이 전송되었음"을 문자(SMS)로 알림

  6. 처리된 상태를 Google 스프레드시트에 기록

올림픽 올림픽 올림픽 올림픽 올림픽

  • 메인함수 중 문자 자동 전송을 위해 추가된 부분


/**
 * 전화번호에서 숫자를 제외한 나머지 문자를 제거하는 헬퍼 함수
 * @param {string} phoneNumber - 원본 전화번호 문자열
 * @return {string} - 숫자만 남은 전화번호 문자열
 */
function cleanPhoneNumber(phoneNumber) {
  return String(phoneNumber).replace(/[^0-9]/g, '');
}

/**
 * 문자 발송 함수
 * - ppurioGetAccessToken() 함수를 통해 액세스 토큰을 발급 받고,
 * - ppurioSendMessage(token, phoneNumber)를 호출하여 문자 메시지를 발송합니다.
 * - 전화번호를 발송 전에 cleanPhoneNumber() 함수를 사용하여 정리합니다.
 * 
 * @param {string} phoneNumber - 발신자(또는 수신자) 전화번호
 * @throws 액세스 토큰 발급 또는 메시지 전송에 실패한 경우 예외 발생
 */
function sendCustomerSMS(phoneNumber) {
  // 전화번호에서 숫자가 아닌 문자를 제거
  const cleanedPhoneNumber = cleanPhoneNumber(phoneNumber);
  Logger.log("Cleaned Phone Number: " + cleanedPhoneNumber);
  
  var token = ppurioGetAccessToken();
  Logger.log("Access Token: " + token);
  
  if (!token) {
    throw new Error("문자 발송 실패: 액세스 토큰 발급에 실패했습니다.");
  }
  
  var messageKey = ppurioSendMessage(token, cleanedPhoneNumber);
  Logger.log("Message sent to " + cleanedPhoneNumber + " with Message Key: " + messageKey);
  
  if (!messageKey) {
    throw new Error("문자 발송 실패: 메시지 발송에 실패했습니다.");
  }
}

/* 
 * 아래의 함수들은 별도 구현 필요:
 * - preparePDFAttachment() : PDF 첨부 파일 준비 함수
 * - generateChatGPTResponse(inquiry) : 문의 내용에 따른 ChatGPT 응답 생성 함수
 * - createEmailContent(name, chatGPTResponse) : 이메일 본문 생성 함수
 * - sendCustomerEmail(email, subject, content, attachment) : 고객에게 이메일 발송 함수
 * - logProcessResult(row, status, message) : 처리 결과 로깅 함수
 * - ppurioGetAccessToken() : 문자 발송 API 액세스 토큰 발급 함수
 * - ppurioSendMessage(token, phoneNumber) : 문자 메시지 전송 함수
 */

  • 문자 자동 전송을 위한 함수


// 전역 상수 설정
const API_URL = "https://message.ppurio.com";
const USER_NAME = "onix7";
const TOKEN = "5af40866e7b1b73c1c5ecd084cd8e578805617ed068e9636da4aa7d49d94517d";
const SEND_TIME = "2025-02-10T16:23:59";

/**
 * HTTP POST 요청을 보내고 JSON 응답을 반환하는 공통 함수
 * @param {string} url - API 엔드포인트 URL
 * @param {Object|null} auth - Basic Auth를 위한 객체 {username, password} (없으면 null)
 * @param {Object|null} payload - 전송할 JSON 객체
 * @param {Object|null} headers - 추가 헤더 정보
 * @return {Object|null} - 응답 JSON 객체 (오류 발생 시 null)
 */
function ppurioMakeRequest(url, auth, payload, headers) {
  if (auth && auth.username && auth.password) {
    var basicAuth = Utilities.base64Encode(auth.username + ":" + auth.password);
    headers = headers || {};
    headers["Authorization"] = "Basic " + basicAuth;
  }
  
  headers = headers || {};
  headers["Content-Type"] = headers["Content-Type"] || "application/json";
  
  var options = {
    method: "post",
    contentType: "application/json",
    payload: payload ? JSON.stringify(payload) : "",
    headers: headers,
    muteHttpExceptions: true
  };
  
  try {
    var response = UrlFetchApp.fetch(url, options);
    var responseCode = response.getResponseCode();
    if (responseCode >= 200 && responseCode < 300) {
      return JSON.parse(response.getContentText());
    } else {
      Logger.log("HTTP Error (" + responseCode + "): " + response.getContentText());
      return null;
    }
  } catch (e) {
    Logger.log("Request exception: " + e);
    return null;
  }
}

/**
 * 엑세스 토큰 발급 (토큰은 24시간 동안 유효)
 * @return {string|null} - access_token 또는 null
 */
function ppurioGetAccessToken() {
  var url = API_URL + "/v1/token";
  var auth = { username: USER_NAME, password: TOKEN };
  var responseData = ppurioMakeRequest(url, auth);
  return responseData ? responseData.token : null;
}

/**
 * 메시지 발송 (예약 발송이 아닌 즉시 발송)
 * @param {string} accessToken - 발급받은 엑세스 토큰
 * @param {string} recipient - 수신자 전화번호
 * @return {string|null} - 발송 후 반환된 messageKey 또는 null
 */
function ppurioSendMessage(accessToken, recipient) {
  var url = API_URL + "/v1/message";
  var headers = {
    "Authorization": "Bearer " + accessToken,
    "Content-Type": "application/json",
  };
  var payload = {
    "account": USER_NAME,              // 뿌리오 계정
    "messageType": "SMS",              // SMS(단문) / LMS(장문) / MMS(포토)
    "content": "문의하신 내용에 대한 답변이 남겨주신 메일로 전송되었습니다. 컷팅스타 드림",  // 메시지 내용
    "from": "01062605500",             // 발신번호 (숫자만)
    "duplicateFlag": "N",              // 수신번호 중복 허용 여부 (Y:허용 / N:제거)
    "rejectType": "AD",                // 광고성 문자 수신거부 설정 (AD:수신거부)
    "refKey": "ref_key",               // 요청에 부여할 키
    "targetCount": 1,                  // 수신자 수
    // 예약 발송은 sendTime 필드를 사용하지 않으므로 즉시 발송됩니다.
    "targets": [
      {
        "to": recipient,             // 매개변수로 전달받은 수신자 번호
        "name": "홍길동",              // [*이름*] 변수에 들어갈 정보 (선택사항)
        "changeWord": { "var1": "10000" } // 치환 변수 정보 (선택사항)
      }
    ]
  };

  var responseData = ppurioMakeRequest(url, null, payload, headers);
  Logger.log("Send message response: " + JSON.stringify(responseData));
  return responseData ? responseData.messageKey : null;
}

/**
 * (예약 발송 취소 함수는 예약 발송 메시지에 대해서만 동작합니다.)
 * 예약 발송 취소는 즉시 발송에는 적용되지 않습니다.
 *
function ppurioCancelReservation(accessToken, messageKey) {
  var url = API_URL + "/v1/cancel";
  var headers = {
    "Authorization": "Bearer " + accessToken,
    "Content-Type": "application/json",
  };
  var payload = {
    "account": USER_NAME,            // 뿌리오 계정
    "messageKey": messageKey         // 고유 메시지 키
  };

  var responseData = ppurioMakeRequest(url, null, payload, headers);
  Logger.log("Cancel response: " + JSON.stringify(responseData));
  return responseData ? responseData.code : null;
}
*/

/**
 * 전체 실행 흐름을 테스트하는 함수
 * 수신자 전화번호를 외부에서 매개변수로 전달받습니다.
 * 만약 전달하지 않으면 기본값 "01062605500"를 사용합니다.
 * 이 테스트 함수는 토큰 발급 후, 지정한 수신번호로 즉시 문자 메시지를 발송합니다.
 */
function testPpurioFlowWithRecipient(recipient) {
  // 전달된 recipient가 없으면 기본값 사용
  var testRecipient = recipient || "01062605500";
  
  var token = ppurioGetAccessToken();
  Logger.log("Access Token: " + token);
  
  if (!token) {
    Logger.log("Test failed: 엑세스 토큰 발급에 실패했습니다.");
    return;
  }
  
  var messageKey = ppurioSendMessage(token, testRecipient);
  Logger.log("Message sent to " + testRecipient + " with Message Key: " + messageKey);
  
  if (!messageKey) {
    Logger.log("Test failed: 메시지 발송에 실패했습니다.");
  } else {
    Logger.log("Test Success: 메시지 발송에 성공하였습니다.");
  }
}

/**
 * 실행 엔트리 포인트
 */
function run() {
  // 원하는 수신자 번호를 전달하여 테스트할 수 있습니다.
  // 예: testPpurioFlowWithRecipient("01011112222");
  testPpurioFlowWithRecipient();
}

📝 시도와 실패

  • 처음에는 솔라피(Solapi) API를 이용해 문자 발송을 시도했으나, 여러 가지 기술적 문제로 인해 최종적으로 다년간 이용해 오던 업체인 **뿌리오(Ppurio)**를 선택하여 구현했습니다.

  • 문자(SMS) 외에도 카카오 알림톡을 활용하는 방안을 검토했으나, 추가적인 인증 절차 및 준비 사항이 많아 추후에 다시 시도해볼 계획입니다.

CRM

📝 구현 결과

  • 고객이 문의를 제출하면 이메일과 동시에 문자로 알림을 받을 수 있도록 개선됨

  • Google Apps Script의 트리거를 활용해 자동화 구현

  • 이메일이 잘 전달되지 않거나 스팸으로 분류될 경우 고객이 즉시 확인 가능

🔮 배운 점

  • Google Apps Script를 활용하면 손쉽게 자동화 시스템을 구축할 수 있음

  • API 연동을 통해 문자 발송 같은 실용적인 기능을 추가할 수 있음

  • 고객 경험을 개선하는 작은 변화가 큰 차이를 만들 수 있음

🌟 다음 계획

  • 고객이 직접 문의 진행 상태를 확인할 수 있도록 추가 개발

  • AI 답변의 정확도를 높이기 위해 ChatGPT API 개선

  • SMS 외에도 카카오 알림톡 같은 추가 채널 연동 검토

Kakoo 비즈니스 앱의 스크린 샷
3
4개의 답글

👉 이 게시글도 읽어보세요