구글폼 고객 문의 AI 자동 분류: 문의가 오면 알아서 줄 세우는 가장 쉬운 방법

🦷 자동화 레시피 · 초보자용
고객 문의가 들어오면,
AI가 분류하고 답변 초안을 만듭니다.

Google Form으로 들어온 고객 문의를 AI가 자동으로 분류하고, 긴급도를 붙이고, 한 줄로 요약하고, 답변 초안까지 적어주는 방법입니다. 코드는 한 덩어리만 붙여 넣으면 됩니다. 어려운 말은 오늘 휴무입니다.

코딩 경험 없어도 가능 답변 자동 발송 안 함 개인정보 기본 마스킹 Google Sheets에서 검토
📝
고객이 문의 작성Google Form으로 접수
📥
시트에 자동 저장새로운 한 줄이 생김
🧠
AI가 문의 분석분류·긴급도·요약
✍️
답변 초안 작성사실은 지어내지 않음
👀
사람이 최종 확인보고 고쳐서 발송
먼저 결론

이 자동화는 고객에게 답변을 자동 발송하지 않습니다. 문의를 분류하고 요약한 뒤 답변 초안을 만들어, 사장님이 확인하고 수정할 수 있게 합니다. 반복 정리는 AI가 맡고, 최종 판단은 사람이 맡습니다.

이 자동화가 해주는 일

🏷️ 문의 종류를 분류합니다

배송, 교환·환불, 상품·서비스, 예약·일정, 결제, 불만·클레임, 제휴·대량구매, 기타로 나눕니다.

🚨 급한 순서를 붙입니다

낮음, 보통, 높음, 즉시 확인으로 구분합니다. 파손, 중복 결제, 개인정보 유출 같은 문의는 위로 올립니다.

🥄 핵심을 한 줄로 요약합니다

긴 문의를 다시 읽지 않아도 되도록 핵심만 짧게 요약합니다. 아침 첫 커피 전에도 이해됩니다.

💬 답변 초안을 만듭니다

확정되지 않은 환불, 배송일, 보상을 지어내지 않고 사람이 확인해야 할 부분을 따로 적습니다.

완성되면 시트가 이렇게 바뀝니다

Google Form 응답 오른쪽에 AI 결과 열이 자동으로 붙습니다. 실제 화면은 아래처럼 생깁니다.

추가되는 열은 AI 분류, AI 긴급도, AI 요약, AI 답변 초안, AI 검토 메모, 처리 상태, AI 처리 시각, AI 오류입니다. 열을 손으로 만들 필요도 없습니다. 코드가 알아서 오른쪽에 붙입니다.

준비물은 네 가지입니다

1️⃣Google 계정

Google Form과 Google Sheets를 사용할 계정입니다.

2️⃣고객 문의용 Google Form

아직 없어도 됩니다. 아래 순서대로 3분이면 만듭니다.

3️⃣OpenAI API 키

ChatGPT 로그인 비밀번호가 아닙니다. API 플랫폼에서 따로 발급하는 키입니다.

4️⃣약 20분

첫 설정만 끝내면 다음 문의부터는 자동입니다.

먼저 알아둘 것: ChatGPT 유료 구독과 OpenAI API 사용료는 별개입니다. API 계정에 결제 수단과 사용 한도가 설정돼 있어야 호출이 작동합니다.

1. Google Form을 만듭니다

1

Google Form에서 새 양식을 만든 뒤 질문을 아래처럼 구성하세요.

  • 이름 또는 업체명: 선택 항목
  • 연락처 또는 이메일: 필요한 경우에만 수집
  • 문의 내용: 필수 항목
  • 원하는 답변 방식: 선택 항목
  • 개인정보 수집·이용 동의: 필요한 고지와 함께 필수 항목

코드는 질문 제목이 문의 내용인 열을 찾습니다. 띄어쓰기 없는 문의내용, 상담 내용, 상담내용도 알아보도록 해두었습니다.

욕심내서 다 받지 마세요

AI 분류에 이름, 전화번호, 상세 주소, 계좌번호는 대개 필요 없습니다. 덜 받으면 덜 새고, 덜 걱정합니다.

2. 응답을 Google Sheets에 연결합니다

2

Google Form 위쪽의 응답 탭을 누르고 초록색 Sheets 아이콘을 선택합니다.

새 스프레드시트 만들기를 선택하면 문의가 들어올 때마다 한 줄씩 쌓이는 응답 시트가 생깁니다.

이제 중요한 갈림길입니다. Apps Script는 Form이 아니라 이 응답 스프레드시트에서 엽니다.

응답 시트 상단 메뉴에서 확장 프로그램 → Apps Script를 선택하세요.

3. API 키를 코드 밖에 숨깁니다

3

Apps Script 왼쪽 메뉴에서 톱니바퀴 모양의 프로젝트 설정을 엽니다.

스크립트 속성에서 새 속성을 추가합니다.

속성: OPENAI_API_KEY
값: 발급받은 OpenAI API 키

API 키를 코드 안에 직접 적지 않는 이유는 간단합니다. 코드를 공유하는 순간 열쇠까지 같이 나갈 수 있기 때문입니다. 열쇠는 주머니에, 코드는 작업대에 둡니다.

4. 완성 코드를 그대로 붙여 넣습니다

Apps Script의 기존 내용을 모두 지우고 아래 코드를 붙여 넣은 뒤 저장하세요. 코드가 길어 보이지만 사용자가 손댈 곳은 거의 없습니다.

전체 Apps Script 코드 펼치기 · 그대로 복사
/**
 * what's in your teeth
 * Google Form → Google Sheets → OpenAI inquiry classifier
 * Checked: 2026-07-19
 *
 * Required Script Property:
 *   OPENAI_API_KEY = your OpenAI API key
 */

const WIYT_CONFIG = Object.freeze({
  MODEL: 'gpt-5.6-luna',
  INQUIRY_HEADERS: ['문의 내용', '문의내용', '상담 내용', '상담내용'],
  CATEGORIES: [
    '배송',
    '교환·환불',
    '상품·서비스',
    '예약·일정',
    '결제',
    '불만·클레임',
    '제휴·대량구매',
    '기타'
  ],
  URGENCIES: ['낮음', '보통', '높음', '즉시 확인'],
  OUTPUT_HEADERS: [
    'AI 분류',
    'AI 긴급도',
    'AI 요약',
    'AI 답변 초안',
    'AI 검토 메모',
    '처리 상태',
    'AI 처리 시각',
    'AI 오류'
  ],
  MAX_INQUIRY_LENGTH: 4000
});

/** 스프레드시트를 열 때 전용 메뉴를 만듭니다. */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('🦷 AI 문의 분류')
    .addItem('1. 자동화 초기 설정', 'setupAutomation')
    .addItem('2. 선택한 행 다시 분류', 'reprocessSelectedRow')
    .addItem('3. OpenAI 연결 테스트', 'testOpenAIConnection')
    .addToUi();
}

/**
 * 한 번만 실행하세요.
 * 출력 열을 만들고, 중복 트리거를 지운 뒤 제출 트리거를 새로 만듭니다.
 */
function setupAutomation() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getActiveSheet();
  const apiKey = getApiKey_();

  if (!apiKey) {
    throw new Error(
      'OPENAI_API_KEY가 없습니다. Apps Script의 프로젝트 설정 → 스크립트 속성에 먼저 저장하세요.'
    );
  }

  ensureOutputHeaders_(sheet);

  ScriptApp.getProjectTriggers()
    .filter((trigger) => trigger.getHandlerFunction() === 'handleFormSubmit')
    .forEach((trigger) => ScriptApp.deleteTrigger(trigger));

  ScriptApp.newTrigger('handleFormSubmit')
    .forSpreadsheet(spreadsheet)
    .onFormSubmit()
    .create();

  SpreadsheetApp.getUi().alert(
    '설정 끝',
    '이제 Google Form에 새 문의가 들어오면 AI가 분류·요약·답변 초안을 작성합니다. 답변은 자동 발송되지 않습니다.',
    SpreadsheetApp.getUi().ButtonSet.OK
  );
}

/** Google Form이 제출될 때 설치형 트리거가 호출합니다. */
function handleFormSubmit(event) {
  const lock = LockService.getScriptLock();
  let sheet = null;
  let row = null;

  try {
    lock.waitLock(30000);

    if (!event || !event.range) {
      throw new Error('폼 제출 이벤트 정보가 없습니다. 이 함수는 직접 실행하지 마세요.');
    }

    sheet = event.range.getSheet();
    row = event.range.getRow();
    processRow_(sheet, row);
  } catch (error) {
    console.error(error);
    if (sheet && row) {
      writeError_(sheet, row, error);
    }
  } finally {
    try {
      lock.releaseLock();
    } catch (ignore) {
      // 잠금을 얻지 못한 경우에는 해제할 것도 없습니다.
    }
  }
}

/** 선택한 행을 수동으로 다시 분류합니다. */
function reprocessSelectedRow() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const row = sheet.getActiveRange().getRow();

  if (row <= 1) {
    throw new Error('헤더가 아닌 문의 행을 선택하세요.');
  }

  processRow_(sheet, row);
  SpreadsheetApp.getUi().alert('선택한 문의를 다시 분석했습니다. 결과를 확인하세요.');
}

/** API 키와 모델 연결을 확인합니다. 소량의 API 사용료가 발생할 수 있습니다. */
function testOpenAIConnection() {
  const result = classifyInquiry_(
    '어제 받은 화분이 깨져서 왔습니다. 사진은 가지고 있고 교환을 원합니다.'
  );

  SpreadsheetApp.getUi().alert(
    '연결 성공',
    [
      '분류: ' + result.category,
      '긴급도: ' + result.urgency,
      '요약: ' + result.summary
    ].join('\n'),
    SpreadsheetApp.getUi().ButtonSet.OK
  );
}

/** 한 행의 문의를 읽고 AI 결과를 같은 행에 씁니다. */
function processRow_(sheet, row) {
  ensureOutputHeaders_(sheet);

  const headers = getHeaders_(sheet);
  const inquiryColumn = findHeaderColumn_(headers, WIYT_CONFIG.INQUIRY_HEADERS);

  if (inquiryColumn === -1) {
    throw new Error(
      '문의 내용 열을 찾지 못했습니다. Google Form 질문 제목을 “문의 내용”으로 만들거나 WIYT_CONFIG.INQUIRY_HEADERS를 수정하세요.'
    );
  }

  const inquiry = String(sheet.getRange(row, inquiryColumn + 1).getDisplayValue()).trim();

  if (!inquiry) {
    writeOutputs_(sheet, row, {
      '처리 상태': '건너뜀',
      'AI 오류': '문의 내용이 비어 있습니다.'
    });
    return;
  }

  writeOutputs_(sheet, row, {
    '처리 상태': 'AI 분석 중…',
    'AI 오류': ''
  });
  SpreadsheetApp.flush();

  const maskedInquiry = sanitizeInquiry_(inquiry);
  const result = classifyInquiry_(maskedInquiry);

  writeOutputs_(sheet, row, {
    'AI 분류': result.category,
    'AI 긴급도': result.urgency,
    'AI 요약': result.summary,
    'AI 답변 초안': result.draft_reply,
    'AI 검토 메모': result.review_note,
    '처리 상태': result.needs_human_review
      ? '사람 확인 필요'
      : '초안 생성 완료 · 발송 전 확인',
    'AI 처리 시각': new Date(),
    'AI 오류': ''
  });

  colorUrgencyCell_(sheet, row, result.urgency);
}

/** OpenAI Responses API를 호출해 구조화된 JSON 결과를 받습니다. */
function classifyInquiry_(maskedInquiry) {
  const apiKey = getApiKey_();

  if (!apiKey) {
    throw new Error('OPENAI_API_KEY가 설정되지 않았습니다.');
  }

  const developerInstruction = [
    '당신은 대한민국의 1인 사업자와 소상공인을 돕는 고객 문의 분류 보조자입니다.',
    '문의 내용을 분류하고 긴급도를 판단하며, 사람이 검토할 짧고 정중한 답변 초안을 작성하세요.',
    '확인되지 않은 주문 상태, 환불 가능 여부, 보상, 재고, 배송일을 만들어내지 마세요.',
    '답변 초안에서 고객에게 민감한 개인정보를 공개 댓글이나 일반 이메일로 보내라고 요구하지 마세요.',
    '안전사고, 법적 위협, 개인정보 유출, 중복 결제, 심각한 폭언·분쟁은 “즉시 확인”으로 분류하세요.',
    '단순 문의는 “낮음”, 일반적인 확인이 필요한 문의는 “보통”, 빠른 사람 확인이 필요한 불만·결제·파손은 “높음”으로 분류하세요.',
    '카테고리는 다음 중 하나만 사용하세요: ' + WIYT_CONFIG.CATEGORIES.join(', '),
    '답변 초안은 2~4문장으로 작성하고, 사실 확인이 필요하면 그 점을 분명히 밝히세요.',
    'review_note에는 사람이 확인해야 할 사실을 한 문장으로 적으세요.'
  ].join('\n');

  const payload = {
    model: WIYT_CONFIG.MODEL,
    input: [
      {
        role: 'system',
        content: developerInstruction
      },
      {
        role: 'user',
        content: maskedInquiry
      }
    ],
    text: {
      format: {
        type: 'json_schema',
        name: 'customer_inquiry_classification',
        strict: true,
        schema: {
          type: 'object',
          properties: {
            category: {
              type: 'string',
              enum: WIYT_CONFIG.CATEGORIES
            },
            urgency: {
              type: 'string',
              enum: WIYT_CONFIG.URGENCIES
            },
            summary: {
              type: 'string'
            },
            draft_reply: {
              type: 'string'
            },
            needs_human_review: {
              type: 'boolean'
            },
            review_note: {
              type: 'string'
            }
          },
          required: [
            'category',
            'urgency',
            'summary',
            'draft_reply',
            'needs_human_review',
            'review_note'
          ],
          additionalProperties: false
        }
      }
    },
    reasoning: { effort: 'low' },
    max_output_tokens: 700,
    store: false
  };

  const response = UrlFetchApp.fetch('https://api.openai.com/v1/responses', {
    method: 'post',
    contentType: 'application/json; charset=utf-8',
    headers: {
      Authorization: 'Bearer ' + apiKey
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  const statusCode = response.getResponseCode();
  const responseText = response.getContentText();
  let body;

  try {
    body = JSON.parse(responseText);
  } catch (error) {
    throw new Error('OpenAI 응답을 JSON으로 읽지 못했습니다. HTTP ' + statusCode);
  }

  if (statusCode < 200 || statusCode >= 300) {
    const apiMessage = body && body.error && body.error.message
      ? body.error.message
      : responseText.slice(0, 500);
    throw new Error('OpenAI API 오류 ' + statusCode + ': ' + apiMessage);
  }

  const outputText = extractOutputText_(body);
  let result;

  try {
    result = JSON.parse(outputText);
  } catch (error) {
    throw new Error('AI 결과 JSON을 읽지 못했습니다: ' + outputText.slice(0, 300));
  }

  return validateResult_(result);
}

/** Responses API 결과에서 최종 텍스트를 찾습니다. */
function extractOutputText_(body) {
  if (body && typeof body.output_text === 'string' && body.output_text) {
    return body.output_text;
  }

  const output = body && Array.isArray(body.output) ? body.output : [];

  for (const item of output) {
    const content = item && Array.isArray(item.content) ? item.content : [];

    for (const part of content) {
      if (part.type === 'output_text' && typeof part.text === 'string') {
        return part.text;
      }

      if (part.type === 'refusal') {
        throw new Error('AI가 요청 처리를 거부했습니다: ' + (part.refusal || '사유 없음'));
      }
    }
  }

  throw new Error('OpenAI 응답에서 출력 텍스트를 찾지 못했습니다.');
}

/** 결과값을 한 번 더 검사합니다. */
function validateResult_(result) {
  if (!result || typeof result !== 'object') {
    throw new Error('AI 결과 형식이 올바르지 않습니다.');
  }

  const category = WIYT_CONFIG.CATEGORIES.includes(result.category)
    ? result.category
    : '기타';
  const urgency = WIYT_CONFIG.URGENCIES.includes(result.urgency)
    ? result.urgency
    : '보통';

  return {
    category: category,
    urgency: urgency,
    summary: String(result.summary || '').trim().slice(0, 300),
    draft_reply: String(result.draft_reply || '').trim().slice(0, 1200),
    needs_human_review: Boolean(result.needs_human_review),
    review_note: String(result.review_note || '').trim().slice(0, 500)
  };
}

/** API로 보내기 전에 흔한 개인정보 패턴을 기본 마스킹합니다. */
function sanitizeInquiry_(text) {
  return String(text)
    .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[이메일 삭제]')
    .replace(/(?:\+82[-.\s]?)?0?1[016789][-.\s]?\d{3,4}[-.\s]?\d{4}/g, '[휴대전화 삭제]')
    .replace(/\b\d{6}[-.\s]?[1-4]\d{6}\b/g, '[주민등록번호 삭제]')
    .replace(/\b(?:\d[ -]*?){13,19}\b/g, '[카드번호 가능성 삭제]')
    .replace(/\b\d{2,4}[-.\s]?\d{2,4}[-.\s]?\d{4,8}\b/g, '[식별번호 삭제]')
    .slice(0, WIYT_CONFIG.MAX_INQUIRY_LENGTH)
    .trim();
}

/** 필요한 출력 열이 없으면 응답 시트 오른쪽에 추가합니다. */
function ensureOutputHeaders_(sheet) {
  const lastColumn = Math.max(sheet.getLastColumn(), 1);
  const headers = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0];
  const missingHeaders = WIYT_CONFIG.OUTPUT_HEADERS.filter(
    (header) => !headers.includes(header)
  );

  if (!missingHeaders.length) {
    return;
  }

  const startColumn = lastColumn + 1;
  sheet.getRange(1, startColumn, 1, missingHeaders.length)
    .setValues([missingHeaders])
    .setFontWeight('bold')
    .setBackground('#C8FF4D')
    .setFontColor('#151515');

  sheet.setFrozenRows(1);
}

/** 첫 행의 헤더를 가져옵니다. */
function getHeaders_(sheet) {
  return sheet
    .getRange(1, 1, 1, Math.max(sheet.getLastColumn(), 1))
    .getDisplayValues()[0]
    .map((header) => String(header).trim());
}

/** 여러 후보명 중 실제 시트에 있는 열 번호를 찾습니다. */
function findHeaderColumn_(headers, aliases) {
  const normalizedAliases = aliases.map(normalizeHeader_);
  return headers.findIndex((header) => normalizedAliases.includes(normalizeHeader_(header)));
}

function normalizeHeader_(text) {
  return String(text).replace(/\s+/g, '').toLowerCase();
}

/** 지정한 헤더의 셀에 결과를 씁니다. */
function writeOutputs_(sheet, row, valuesByHeader) {
  const headers = getHeaders_(sheet);

  Object.keys(valuesByHeader).forEach((header) => {
    const columnIndex = headers.indexOf(header);
    if (columnIndex === -1) {
      return;
    }

    const cell = sheet.getRange(row, columnIndex + 1);
    const value = valuesByHeader[header];
    cell.setValue(value);

    if (header === 'AI 처리 시각' && value instanceof Date) {
      cell.setNumberFormat('yyyy-mm-dd hh:mm:ss');
    }
  });
}

/** 오류가 생겨도 문의 원문은 남겨 두고 오류 열에 이유를 기록합니다. */
function writeError_(sheet, row, error) {
  try {
    ensureOutputHeaders_(sheet);
    writeOutputs_(sheet, row, {
      '처리 상태': '오류 · 다시 확인',
      'AI 오류': error && error.message ? error.message : String(error)
    });
  } catch (secondaryError) {
    console.error(secondaryError);
  }
}

/** 긴급도 셀에 가벼운 색 표시를 넣습니다. */
function colorUrgencyCell_(sheet, row, urgency) {
  const headers = getHeaders_(sheet);
  const columnIndex = headers.indexOf('AI 긴급도');

  if (columnIndex === -1) {
    return;
  }

  const colors = {
    '낮음': '#E5FFF2',
    '보통': '#FFF7CC',
    '높음': '#FFE7D6',
    '즉시 확인': '#FFE8EE'
  };

  sheet.getRange(row, columnIndex + 1)
    .setBackground(colors[urgency] || '#FFFFFF')
    .setFontWeight('bold')
    .setFontColor('#151515');
}

/** 스크립트 속성에서 API 키를 읽습니다. */
function getApiKey_() {
  return PropertiesService
    .getScriptProperties()
    .getProperty('OPENAI_API_KEY');
}
업종에 맞게 바꾸고 싶다면: 코드 맨 위의 WIYT_CONFIG.CATEGORIES만 수정하면 됩니다. 예를 들어 식물 매장은 배송, 식물 관리, 반려동물 안전성, 파손, 교환·환불, 대량구매처럼 바꿀 수 있습니다.

5. 초기 설정을 한 번 실행합니다

5

Apps Script 위쪽 함수 선택 메뉴에서 setupAutomation을 선택하고 실행 버튼을 누릅니다.

처음 한 번은 Google 권한 승인 화면이 나옵니다. 이 스크립트는 응답 시트를 읽고 쓰며, 외부 API를 호출하고, Form 제출 트리거를 만드는 권한이 필요합니다.

설정이 끝나면 응답 시트 오른쪽에 AI 결과 열이 자동으로 만들어집니다.

handleFormSubmit은 직접 실행하지 마세요

이 함수는 실제 Form이 제출될 때 Google이 전달하는 이벤트 정보를 받아 움직입니다. 직접 실행하면 “이벤트 정보가 없습니다”라고 나오는 게 정상입니다.

6. 테스트 문의를 하나 넣습니다

6

Google Form 미리보기에서 아래처럼 테스트 문의를 제출해 보세요.

어제 받은 화분이 깨져서 왔습니다. 사진은 가지고 있고 교환을 원합니다.

몇 초 뒤 응답 시트에 분류, 긴급도, 요약, 답변 초안이 채워지면 성공입니다.

시트를 새로 열면 상단에 🦷 AI 문의 분류 메뉴도 생깁니다. 선택한 행 다시 분류와 연결 테스트를 여기서 실행할 수 있습니다.

AI는 이런 식으로 정리합니다

📥 들어온 문의

어제 받은 화분이 깨져서 왔습니다. 사진은 가지고 있고 교환을 원합니다.

🧠 AI가 정리한 결과

분류: 교환·환불
긴급도: 높음
요약: 파손 배송으로 교환을 요청한 문의

💬 답변 초안

배송 중 파손으로 불편을 드려 죄송합니다. 사진과 주문 정보를 확인한 뒤 가능한 처리 방법을 안내드리겠습니다. 확인 전에는 교환 가능 여부를 확정하지 않습니다.

👀 사람이 볼 메모

주문 내역, 상품 파손 사진, 실제 교환 기준과 재고를 확인해야 합니다.

개인정보를 그대로 AI에 보내지 마세요

코드에는 이메일, 휴대전화 번호, 주민등록번호, 카드번호로 보이는 숫자를 API 전송 전에 마스킹하는 기능을 넣었습니다.

전송 전 홍길동 / 010-1234-5678
order@example.com
주문번호 20260719-12345
AI에 전달 고객 / [휴대전화 삭제]
[이메일 삭제]
[식별번호 삭제]
마스킹은 안전벨트이지 방탄차가 아닙니다. 주소, 사람 이름, 특이한 주문번호처럼 정규식으로 완벽히 잡기 어려운 정보도 있습니다. 고객 문의 Form에서 불필요한 개인정보를 처음부터 받지 않는 것이 가장 안전합니다.

OpenAI API의 입력과 출력은 기본 설정상 모델 학습에 사용되지 않지만, 서비스 제공과 남용 탐지를 위한 데이터 보존 정책은 별도로 적용될 수 있습니다. 코드의 store: false도 법적 의미의 데이터 미보존을 자동 보장하는 버튼은 아닙니다. 민감정보를 보내지 않는 설계가 먼저입니다.

안 될 때는 여기부터 봅니다

OPENAI_API_KEY가 없다고 나옵니다
Apps Script의 프로젝트 설정 → 스크립트 속성에 속성 이름을 정확히 OPENAI_API_KEY로 저장했는지 확인하세요. 앞뒤 공백도 지웁니다.
문의 내용 열을 찾지 못했다고 나옵니다
Google Form 질문 제목을 문의 내용으로 바꾸세요. 다른 이름을 쓰려면 코드 맨 위의 INQUIRY_HEADERS에 현재 질문 제목을 추가합니다.
새 문의가 들어와도 아무 일도 없습니다
setupAutomation을 실행했는지 확인하세요. Apps Script 왼쪽의 트리거 메뉴에서 handleFormSubmit 제출 트리거가 있는지도 확인합니다.
401 오류가 납니다
API 키가 잘못됐거나 폐기된 경우가 많습니다. OpenAI API 플랫폼에서 새 키를 발급하고 스크립트 속성 값을 교체하세요.
429 오류가 납니다
사용 한도, 결제 상태 또는 요청 제한을 확인하세요. 짧은 시간에 문의가 몰린 경우 잠시 뒤 선택한 행 다시 분류를 실행할 수 있습니다.
모델을 사용할 수 없다고 나옵니다
코드 상단의 MODEL 값을 현재 API 계정에서 사용할 수 있는 텍스트 모델로 바꾸세요. 이 글은 2026년 7월 19일 기준 gpt-5.6-luna를 사용합니다.

비용은 어디서 생기나

Google Form과 개인용 Google Sheets는 기본 사용 범위 안에서는 별도 자동화 도구 구독 없이 구성할 수 있습니다. 비용이 생기는 부분은 주로 OpenAI API 사용량입니다.

  • 문의가 짧을수록 입력 토큰이 줄어듭니다.
  • 답변 초안을 짧게 제한하면 출력 토큰이 줄어듭니다.
  • 같은 문의를 반복 재처리하면 그만큼 API 호출이 다시 발생합니다.
  • API 대시보드에서 월 사용 한도를 낮게 설정해 두는 것이 안전합니다.

이 글의 기본 모델은 비용 민감형 작업에 맞춘 gpt-5.6-luna입니다. 모델 가격과 이용 가능 여부는 바뀔 수 있으므로 실제 적용 전 공식 모델 페이지를 다시 확인하세요.

여기까지 자동화하고, 여기서 멈추세요

자동화해도 좋은 일

분류, 긴급도 표시, 한 줄 요약, 답변 초안, 사람이 확인할 항목 정리

🛑 사람이 결정할 일

환불 승인, 보상 약속, 계약·법률 판단, 안전사고 대응, 개인정보 처리, 최종 답변 발송

AI는 문의 정리와 답변 초안까지 맡을 수 있지만, 최종 결정과 책임은 사업자에게 있습니다. 처음에는 테스트 문의 10개로 돌려보고, 분류 기준과 답변 말투를 다듬은 뒤 실제 문의에 적용하세요.

공식 자료

이전 글 다음 글