유튜브 자동 댓글 챗봇 만들기 (구글시트+Gemini 활용)
유튜브 챗봇 만들기 목차 바로가기
영상 강의
예제파일 다운로드
오빠두엑셀의 강의 예제파일은 여러분을 위해 자유롭게 제공하고 있습니다.
- [업무생산성] 유튜브 자동 댓글 챗봇 만들기보충파일
라이브 강의 전체영상도 함께 확인해보세요!
위캔두 회원이 되시면 매주 오빠두엑셀에서 진행하는 라이브강의 풀영상을 확인하실 수 있습니다.
관련 링크 모음
- Gemini API 발급
- 구글 클라우드 플랫폼
- 구글시트 oAuth 스크립트 ID
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
- Gemini 무료 API 요금 안내
- 유튜브 API 작업별 Cost 안내
Gemini 연동 마스터 코드
var scriptProperties = PropertiesService.getScriptProperties(); const geminiApiKey = scriptProperties.getProperty('GEMINI_API_KEY'); const openAi_ApiKey = scriptProperties.getProperty('OPENAI_API_KEY'); const geminiModel = 'gemini-1.5-flash'; // 다른 버전의 Gemini 모델 필요시, 여기서 수정하세요. const geminiEndpoint = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}-latest:generateContent?key=${geminiApiKey}`; /** ContetPrompt : 맥락, 상황을 작성합니다.(예: 채널 설명 등) tontPrompt : 어조를 지정합니다. (예: 부드럽게, 유머스럽게 등) instructionPrompt : 그 외 지시사항을 작성합니다. (예: 답글 마지막에는 '감사합니다!'로 끝낼 것 등) **/ const contextPrompt = `* 나는 대한민국에서 직장인을 위해 엑셀 및 다양한 오피스 프로그램의 활용법을 알려주는 유튜브 채널을 운영하고 있어. * 구독자가 작성한 댓글에 답변을 작성하는 작업에 도움이 필요해.`; const tonePrompt = `* 답변은 친절하면서 간결하고, 명료하고, 전문성있게 작성해. 항상 전문성을 잃지 않도록 노력해.`; const instructionPrompt = `* 마지막에는 "감사합니다! -오빠두봇드림✨"으로 마칠 것. * 댓글 내용은 반복하지 말고, 답글만 작성할 것. * 만약 댓글에 타임라인(예: 12:34)이 있으면 커리큘럼에 있는 타임라인을 참고해서 응원의 문장을 작성할 것.\n`; //-------------------------------------------- function 프롬프트미리보기 () { generatePrompt('영상 제목','영상 설명','감사합니다!','별말씀을요!'); } function YoutubeCommentAutoReply() { GetCommentsInSheet(); generateReplies(); postRepliesToComments(); } function generateReplies() { var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var dataRange = sheet.getDataRange(); var data = dataRange.getValues(); var headers = data[0]; var idIndex = headers.indexOf("ID"); var titleIndex = headers.indexOf("영상 제목"); var descriptionIndex = headers.indexOf("영상 설명"); var textIndex = headers.indexOf("댓글"); var replyIndex = headers.indexOf("답글"); var keywordIndex = headers.indexOf("초안"); var nameIndex = headers.indexOf("닉네임"); var statusIndex = headers.indexOf("확인"); for (var i = data.length - 1; i >= 1; i--) { if (data[i][statusIndex] === '') { if (data[i][nameIndex] === yHandle) { sheet.getRange(i + 1, replyIndex + 1).setValue('-'); sheet.getRange(i + 1, statusIndex + 1).setValue('X'); } else { var commentId = data[i][idIndex]; var youtubeTitle = data[i][titleIndex]; var youtubeDescription = data[i][descriptionIndex]; var comment = data[i][textIndex]; var keyword = data[i][keywordIndex]; sheet.getRange(i + 1, replyIndex + 1).setValue('✨ 답변 생성중...'); SpreadsheetApp.flush(); if (commentId.includes('.')) { var topLevelCommentId = commentId.split('.')[0]; var previousComments = ''; for (var j = 1; j < data.length - 1; j++) { var prevCommentId = data[j][idIndex]; if (prevCommentId === topLevelCommentId || prevCommentId.startsWith(topLevelCommentId + '.')) { var prevComment = data[j][textIndex]; var prevReply = data[j][replyIndex]; previousComments += 'Comment: ' + prevComment + '\nReply: ' + prevReply + '\n\n'; } } var prompt = generatePrompt(youtubeTitle, youtubeDescription, comment, keyword, previousComments); var reply = askGemini(prompt); // <- ChatGPT 사용 시, 이 부분을 askChatGPT로 변경하세요! sheet.getRange(i + 1, replyIndex + 1).setValue(reply); } else { var prompt = generatePrompt(youtubeTitle, youtubeDescription, comment, keyword); var reply = askGemini(prompt); // <- ChatGPT 사용 시, 이 부분을 askChatGPT로 변경하세요! sheet.getRange(i + 1, replyIndex + 1).setValue(reply); } } } else if (data[i][statusIndex] === 'O') { break; } else { sheet.getRange(i + 1, replyIndex + 1).setValue('-'); } SpreadsheetApp.flush(); } } function generatePrompt(youtubeTitle='', youtubeDescription='', comment='', keyword='', previousComments='') { var prompt = `###Context###\n${contextPrompt}\n\n`; prompt += `###Objective###\n* Information에 제공한 영상 제목과 설명을 참고하여, 댓글에 대한 답변을 작성해.\n`; if (keyword.length > 0) { prompt += `* 답변은 초안을 참고하여 작성할 것.\n`; } prompt += '\n'; prompt += `###Information###\n`; prompt += `* 영상 제목: ${youtubeTitle}\n* 영상 설명: ${youtubeDescription}\n`; if (previousComments) { prompt += `* 이전 댓글과 답변:\n${previousComments}\n`; } if (keyword.length > 0) { prompt += `* 댓글: ${comment}\n* 답변 초안: ${keyword}\n\n`; } else { prompt += `* 댓글: ${comment}\n\n`; } prompt += `###Tone###\n${tonePrompt}`; prompt += '\n\n'; prompt += `###Intruction###\n` + `${instructionPrompt}`; Logger.log(prompt); return prompt; } /** * Gemini API를 사용하여 응답을 생성합니다. * @param {string} prompt - Gemini 모델에 전달할 프롬프트 텍스트 * @param {number} temperature - 생성 텍스트의 무작위성을 제어하는 온도 값 (기본값: 1) * @return {string} Gemini 모델이 생성한 텍스트 응답 * @customfunction */ function askGemini(prompt, temperature=1) { const payload = { "contents": [ { "parts": [ { "text": prompt } ] } ], "generationConfig": { "temperature": temperature, }, }; const options = { 'method' : 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload) }; const response = UrlFetchApp.fetch(geminiEndpoint, options); const data = JSON.parse(response); const content = data["candidates"][0]["content"]["parts"][0]["text"]; return content; } function askChatGPT(prompt) { var url = 'https://api.openai.com/v1/chat/completions'; var options = { method: 'post', contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + openAi_ApiKey }, payload: JSON.stringify({ model: 'gpt-4o-mini', // Use the correct model name messages: [ {role: 'system', content: '당신은 유튜브 채널에 남겨진 구독자 댓글에 대한 답변을 작성하는 대한민국 최고의 챗봇입니다.'}, {role: 'user', content: prompt} ] }), muteHttpExceptions: true }; var response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() !== 200) { Logger.log('Error: ' + response.getContentText()); throw new Error('Failed to fetch response from OpenAI API'); } var json = response.getContentText(); var reply = JSON.parse(json).choices[0].message.content.trim(); return reply; }
유튜브 데이터 API 연동 마스터 코드
const oAuthURL = 'https://accounts.google.com/o/oauth2/auth'; const tokenURL = 'https://accounts.google.com/o/oauth2/token'; const sslScope = 'https://www.googleapis.com/auth/youtube.force-ssl'; var scriptProperties = PropertiesService.getScriptProperties(); const clientId = scriptProperties.getProperty('CLIENT_ID'); const yHandle = scriptProperties.getProperty('YOUTUBE_HANDLE'); const channelId = scriptProperties.getProperty('YOUTUBE_CHANNEL_ID'); const sheetId = scriptProperties.getProperty('SHEET_ID'); function getYouTubeService() { return OAuth2.createService('YouTube') .setAuthorizationBaseUrl(oAuthURL) .setTokenUrl(tokenURL) .setClientId(clientId) .setCallbackFunction('authCallback') .setPropertyStore(PropertiesService.getUserProperties()) .setScope(sslScope) .setParam('access_type', 'offline') .setParam('approval_prompt', 'force') .setParam('login_hint', Session.getActiveUser().getEmail()); } function authCallback(request) { var youtubeService = getYouTubeService(); var isAuthorized = youtubeService.handleCallback(request); if (isAuthorized) { return HtmlService.createHtmlOutput('인증에 성공하였습니다. 이 창을 종료 후, 앱스크립트 편집기로 돌아가도 괜찮습니다.'); } else { return HtmlService.createHtmlOutput('인증 과정에 오류가 발생했습니다. 다시 시도하세요.'); } } function 답글생성하기() { generateReplies(); } function 댓글불러오기() { var youtubeService = YouTube.CommentThreads; var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var lastRow = sheet.getLastRow(); var lastTimestamp = lastRow > 1 ? new Date(sheet.getRange(lastRow, 6).getValue()) : null; var videoResponse = youtubeService.list('snippet,replies', { allThreadsRelatedToChannelId: channelId, maxResults: 100 }); var commentThreads = []; for (var i = 0; i <= videoResponse.items.length - 1 ; i++) { var item = videoResponse.items[i]; var videoId = item.snippet.videoId; if (!videoId) { Logger.log('잘못된 Video ID 입니다. : ' + videoId); continue; } var videoDetails = YouTube.Videos.list('snippet', { id: videoId }).items[0]; if (!videoDetails) { Logger.log('영상 설명을 찾을 수 없습니다. : ' + videoId); continue; } if (item.snippet.topLevelComment.snippet.authorDisplayName != yHandle) { commentThreads.unshift([ item.snippet.topLevelComment.id, videoDetails.snippet.title, videoDetails.snippet.description, item.snippet.topLevelComment.snippet.textDisplay.replace(/<[^>]*>/g, ''), item.snippet.topLevelComment.snippet.authorDisplayName, item.snippet.topLevelComment.snippet.publishedAt ]); } if (item.replies) { for (var j = 0; j <= item.replies.comments.length - 1; j++) { var reply = item.replies.comments[j]; if (reply.snippet.authorDisplayName != yHandle) { commentThreads.unshift([ reply.id, videoDetails.snippet.title, videoDetails.snippet.description, reply.snippet.textDisplay.replace(/<[^>]*>/g, ''), reply.snippet.authorDisplayName, reply.snippet.publishedAt ]); } } } } var newCommentThreads = lastTimestamp ? commentThreads.filter(function(comment) { var commentTimestamp = new Date(comment[5]); return commentTimestamp > lastTimestamp; }) : commentThreads; newCommentThreads.sort(function(a, b) { var timeA = new Date(a[5]); var timeB = new Date(b[5]); return timeA - timeB; }); if (newCommentThreads.length > 0) { sheet.getRange(lastRow + 1, 1, newCommentThreads.length, newCommentThreads[0].length).setValues(newCommentThreads); } } function 답글업로드하기() { var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var lastRow = sheet.getLastRow(); var idIndex = 1; var replyIndex = 9; var statusIndex = 7; var youtubeService = YouTube.Comments; for (var i = lastRow; i >= 2; i--) { var commentId = sheet.getRange(i, idIndex).getValue(); var reply = sheet.getRange(i, replyIndex).getValue(); var status = sheet.getRange(i, statusIndex).getValue(); if (status == 'O') { break; } Logger.log('Comment ID: ' + commentId); Logger.log('Original Reply: ' + reply); Logger.log('Status: ' + status); var topLevelCommentId = commentId.split('.')[0]; if (reply.trim().length > 0 && status == '') { var resource = { snippet: { parentId: topLevelCommentId, textOriginal: reply } }; try { youtubeService.insert(resource, 'snippet'); sheet.getRange(i, statusIndex).setValue('O'); Logger.log('ID의 답글을 성공적으로 게시했습니다. : ' + commentId); } catch (error) { Logger.log('해당 ID의 답글 게시 중 오류가 발생했습니다. : ' + commentId); Logger.log('오류 메시지 : ' + error.message); } } else { Logger.log('해당 ID의 답글이 비어있어 다음 답글로 넘어갑니다. : ' + commentId); } } }
ChatGPT 기반 챗봇 만드는 방법
Gemini 무료 API 대신 ChatGPT 기반의 챗봇을 사용하려면, 다음 단계에 따라 OpenAI에서 API키를 발급받은 후 코드를 수정합니다.
- 아래 링크를 클릭하여 OpenAI 개발자센터로 이동 후 API키를 발급받습니다.
https://platform.openai.com/api-keys
- Dashboard → API Keys → [Create New secret key] 버튼 클릭
- '만약 등록한 결제 수단이 없을 경우, API키 발급이 안될 수 있습니다. 그럴 경우 우측 상단의 [설정] → Billing 에서 결제수단을 먼저 등록합니다.

OpenAI 개발자센터에서 API키를 발급받습니다. 오빠두Tip : OpenAI API키를 발급받는 전체 과정 및 엑셀+ChatGPT 추가기능 사용법은 아래 영상 강의를 확인하세요!👇
- 앱스크립트 편집기로 이동한 후, 속성에서 새로운 속성값을 추가합니다.
Property : OPENAI_API_KEY
Value : OpenAI API키
발급받은 API키를 구글시트 앱스크립트 속성에 추가합니다. - Gemini 연동 마스터 코드 중, askGemini를 askChatGPT로 변경하면 ChatGPT API를 활용한 유튜브 챗봇이 완성됩니다. (두 군데를 모두 바꿔주세요!)
변경 전 : var reply = askGemini(prompt);
변경 후 : var reply = askChatGPT(prompt);
Gemini 연동 마스터 코드에서, askGemini 함수를 askChatGPT로 수정합니다.

generateReplies@ Code.gs:61답글생성하기@ YoutubeAPI.gs:34
youtubeapi appscript 에서 댓글생성하기 누르면 위 오류가 나는데 왜그런지요??ㅠㅠ
undefined 오륜느 includes 를 호출한 개체, 'commendId'가 올바르게 선언되지 않아서 발생합니다.
코드를 디버깅하셔서, 오류가 발생하는 전 단계에 Logger.log(commendID); 를 추가해서 commendId가 올바르게 선언되었는지 확인해보시길 바랍니다.
감사합니다.
ChatGPT를 통한 생성으로 변경 후 다음과 같은 오류가 발생하는 데 해결하는 방법이 있을까요?
오류 메시지 : API call to youtube.comments.insert failed with error: The comment cannot be created due to insufficient permissions. The request might not be properly authorized.
해당 오류는 Youtube API에 댓글 작성에 대한 권한이 올바르게 주어지지 않아 발생하는 것으로 보입니다.
API 설정을 다시 한번 확인해보세요 :)